/* Shared values and controls live in ui.css; this sheet owns page composition. */
* { margin: 0; padding: 0; box-sizing: border-box; }

/* The universal reset above also clears the UA's `dialog { margin: auto }`,
   which is what centres a native modal dialog in its viewport — without it
   every `showModal()` surface lands in the top-left corner. Restore it for
   dialogs as a class, not per dialog. */
dialog { margin: auto; }


[hidden] {
    display: none !important;
}

/* A page laid out wider than its screen (Interface size below 100 %, theme.js) keeps its type scale: a phone's
   text inflation would enlarge long text against the owner's size. At 100 % the phone's own text settings stand. */
:root[data-interface-smaller] { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

body {
    font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
    background: var(--bg-primary);
    color: var(--text-primary);
    height: var(--vvh);
    overflow: hidden;
}

/* The sidebar's column is as wide as the sidebar's own box (its width below), and the dimmed room and the Project
   panel are placed in the columns after it: everything that lines up with the sidebar follows its box by layout.
   So a drag of a handle writes the width it measures into the dragged box's own --drag-width, which no element
   inherits, and a frame restyles that one box; the stored width on :root (--sidebar-width, --project-panel-width),
   which every element inherits, is written once, when the drag ends (app.js). */
@property --drag-width {
    syntax: '*';
    inherits: false;
}
/* The phone drawer's finger (drawer_drag.js) moves by one number on the drawer and its scrim: not inherited, so a
   frame restyles those two boxes, not every row inside the drawer. */
@property --drawer-p {
    syntax: '<number>';
    inherits: false;
    initial-value: 0;
}
/* The finger's settle (drawer_drag.js writes it on the drawer, its scrim and the room card) is not inherited either:
   written on the room card it would restyle every row of the room at the release, the moment the drawer latches. */
@property --drawer-settle {
    syntax: '*';
    inherits: false;
}

#app {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    height: var(--vvh);
    position: relative;
    /* The docked Project panel slides in from 18px past the window's edge; clipped here, that offset never widens
       the page, so a scrollIntoView during the slide cannot scroll the whole window sideways. */
    overflow-x: clip;
}

/* An older desktop page can survive a source update with its old #divider in the DOM.
   Keep that retired grid child out of flow while the WebView reloads newer assets. */
#app > #divider { display: none; }
/* Its old reconnect veil too (7.7.0 and older; the veil is gone, owner 20.10 Q4): never shown, never taking room. */
#reconnect-overlay { display: none !important; }

/* App navigation shell: a docked sidebar column, a drawer in the stack layout. */
/* The shell's icon controls (☰, the drawer's ×, a Project's ×): one round neutral capsule holding one glyph, the
   chrome height on a desk and the touch minimum in the stack layout. Material: the "Airy dopamine" tail. */
.shell-icon-control {
    flex: 0 0 auto;
    width: var(--control-h-float);
    height: var(--control-h-float);
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid transparent;
    border-radius: var(--pill-radius);
    background: var(--chip-float);
    color: var(--text-primary);
    box-shadow: var(--elev-rest);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    cursor: pointer;
    transition: var(--motion-tone);
}
@media (hover: hover) {
    .shell-icon-control:hover { background: var(--chip-float-hover); box-shadow: var(--elev-rest); }
}
.shell-icon-control:not(:disabled):active { filter: var(--press); }
.shell-icon-control:disabled { box-shadow: none; }
.shell-icon-control > svg {
    display: block;
    width: 20px;
    height: 20px;
}
html[data-layout="stack"] .shell-icon-control {
    width: 44px;
    height: 44px;
}
/* A desk has a docked sidebar: no ☰ in the headers and no drawer head (the stack layout shows both). */
.mobile-nav-toggle,
.drawer-head {
    display: none;
}

.nav-drawer-backdrop {
    display: none;
}

/* A layer's root (the phone drawer, the Project panel) takes the focus as it opens: a landing point, never a control,
   so it draws no ring, the engine's blue one included (layers.js, U122); the controls inside keep theirs. */
[data-layer-landing]:focus { outline: none; }

/* The sidebar's zones (docs/DESIGN.md "Shell"): a pinned head (Main Chat), the Projects list as the ONE scroller, the
   tools right after it, and the foot (the version and the update) at the bottom edge; free room lies between the
   tools and the foot, never between the list and the tools. Row
   heights follow the pointer: a finger gets the touch minimum, a mouse a quieter row, and a short window with a
   mouse compacts the rows first. A window too short even for that falls back to the whole column scrolling with
   the head sticky and the foot still on the bottom edge. */
#primary-sidebar {
    --nav-row-h: 44px;       /* a place: Main Chat, Files … */
    --nav-item-h: 32px;      /* a Project row */
    --nav-control-h: 28px;   /* a row's ⋯ and the header's + */
    --nav-kebab-w: var(--nav-control-h);  /* the column a Project row's ⋯ takes (none on touch: a long press opens it) */
    --nav-label-x: calc(var(--space-3) + 18px + var(--space-3));  /* the one text column every label starts on */
    --sidebar-pad-bottom: var(--space-3);  /* the foot's inset from the bottom edge; the drawer adds the safe area */
    width: var(--drag-width, var(--sidebar-width));
    height: var(--vvh);
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
    padding: 0 var(--space-3) var(--sidebar-pad-bottom);
    background: var(--surface-side);
    border-right: 1px solid rgba(var(--neutral-rgb), 0.05);
    z-index: var(--z-sidebar);
    position: relative;  /* positioning context for the resize handle */
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
}
@media (pointer: coarse) {
    #primary-sidebar { --nav-item-h: 44px; --nav-control-h: 44px; }
}
@media (pointer: fine) and (max-height: 700px) {
    #primary-sidebar { --nav-row-h: 36px; --nav-item-h: 28px; }
}
/* A window too short for the zones (the head, three list rows and the foot no longer fit): the whole column is
   the ONE scroller with the head sticky, and the list shows every row in flow, so a finger or a wheel moves one
   thing (the gate counts the sidebar's scrollers). Finger rows are 44px, so a touch column runs out of room
   sooner (measured: 586px of zones with 30 Projects). The foot, the version and the update, stays on the bottom
   edge there too (owner 23.10): the tools' box steps aside (its rows become the column's), so the foot sticks to
   the column itself and the rows pass under it as they pass under the head. Its offset and negative margin reach
   through the column's bottom padding, which it takes as its own, so its text sits where a tall window puts it.
   The two bars' measured heights (layout.js measurePinnedBars) are the scroll margin of the rows that pass under
   them, so a row reached with Tab is never left under either bar, while a bar's own control (Main, the update)
   leaves the column where it is (a scroll padding on the column would count the bars as covered and move it). */
@media (max-height: 540px), (pointer: coarse) and (max-height: 600px) {
    #primary-sidebar :is(.nav-projects *, .sidebar-foot > [data-nav-page]) {
        scroll-margin-block: var(--sidebar-head-h, 0px) var(--sidebar-foot-h, 0px);
    }
    #primary-sidebar .nav-projects { flex: 0 0 auto; min-height: 0; }
    #primary-sidebar .nav-projects-list { flex: 0 0 auto; overflow-y: visible; }
    #primary-sidebar .sidebar-foot { display: contents; }
    #primary-sidebar .sidebar-foot > :first-child { margin-top: var(--space-2); }  /* the box's padding-top */
    #primary-sidebar .sidebar-foot-meta {
        position: sticky;
        bottom: calc(-1 * var(--sidebar-pad-bottom));
        z-index: 1;
        margin-bottom: calc(-1 * var(--sidebar-pad-bottom));
        padding-bottom: var(--sidebar-pad-bottom);
        background: var(--surface-side);
    }
}

.sidebar-head {
    position: sticky;
    top: 0;
    z-index: 1;
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding-top: var(--space-4);
    background: var(--surface-side);
}

/* The tools follow the Projects; the box takes the column's free room, so its last row, the version and the update,
   rests on the bottom edge (owner 22.10) whether the list is collapsed, short or scrolling. A window too short for
   the zones scrolls the whole column instead, and the foot sticks to the bottom edge (the fallback above). */
.sidebar-foot {
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding-top: var(--space-2);
}

.sidebar-foot-meta {
    margin-top: auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-2) 0 var(--space-3);
}

/* An older desktop page can also survive a source update with the pre-three-zone .sidebar-scroll wrapping the
   column's rows. As a box it left the list as tall as its rows: a scroller with nothing to scroll whose containment
   kept the wheel from the column in Chromium, while WebKit scrolled the whole column. Without its box the
   rows are the column's again: the Projects list scrolls between Main and the tools, and a short window keeps the
   whole-column fallback above. */
#primary-sidebar > .sidebar-scroll { display: contents; }

/* macOS desktop window (launcher_window_chrome.py): while its transparent title bar covers the page,
   the shell writes the band's height and the traffic lights' far edge here, measured, in CSS px.
   The sidebar starts below the band; content reaching the left edge (drawer layout, a project panel
   as wide as the window) starts past the lights. Without the shell's attribute nothing moves. */
:root[data-window-chrome="integrated"] {
    --window-chrome-top: 0px;
    --window-chrome-start: 0px;
    --window-chrome-clearance: calc(var(--window-chrome-start) + var(--space-3));
}
:root[data-window-chrome="integrated"] .sidebar-head {
    padding-top: calc(var(--space-4) + var(--window-chrome-top));
}
:root[data-window-chrome="integrated"] .onboarding-overlay {
    box-sizing: border-box;
    padding-top: var(--window-chrome-top);
}
/* Each room's header floats over its transcript and lets a press between its groups through to the row beneath, a
   link or button the owner cannot see: inside the band this strip takes such a press for the title bar. The
   header's groups stay above it; the feed keeps its scrollbar (the global 6px ::-webkit-scrollbar). */
:root[data-window-chrome="integrated"] :is(.chat-page-header, .project-panel-bar)::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 6px;
    height: var(--window-chrome-top);
    z-index: -1;
    pointer-events: auto;
}
:root[data-window-chrome="integrated"][data-layout="stack"] .project-panel-bar { padding-left: max(14px, var(--window-chrome-clearance)); }
:root[data-window-chrome="integrated"][data-layout="stack"] .drawer-head {
    padding-top: max(calc(var(--chrome-inset) + var(--inset-top)), calc(var(--space-2) + var(--window-chrome-top)));
}
:root[data-window-chrome="integrated"][data-layout="stack"] .app-page-header { padding-left: max(12px, var(--window-chrome-clearance)); }

/* Resizable side-section drag handles (v6.33.0). Thin edge strips that resize the sidebar and the Project panel
   (app.js: --drag-width while dragged, the stored width when the drag ends); absent in the stack layout (layout.js). */
/* CW10 (v6.34.0): suppress text selection while dragging via a class, not a JS
   inline style (DEVELOPMENT.md: no element.style.* property mutations in UI JS). */
body.resizing-panels { user-select: none; }
.resize-handle {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 8px;
    cursor: col-resize;
    z-index: var(--z-resize-handle);
    touch-action: none;
}
.resize-handle-right { right: 0; }
.resize-handle-left { left: -4px; }
@media (hover: hover) {
    .resize-handle:hover { background: var(--accent-10); }
}
html[data-layout="stack"] .resize-handle { display: none; }

/* The version is a readable fact in the meta ink (owner Q6=A), beside the update capsule. */
#nav-version {
    display: block;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.nav-row,
.nav-section-toggle {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: var(--space-3);
    min-height: var(--nav-row-h);
    padding: 0 var(--space-3);
    width: 100%;
    border: 1px solid transparent;
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
    border-radius: var(--radius-inner);
    font-size: var(--type-body);
    font-family: inherit;
    text-align: left;
    position: relative;
}

.nav-row svg,
.nav-section-toggle svg {
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
}

.nav-row-icon {
    flex: 0 0 18px;
    width: 18px;
    height: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.nav-row-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The sidebar's marks are the shared status marks (ui.css "Status marks", status_mark.js): one per place, in the
   place's own ink (hover and the chosen row's ink included), the owner's amber and the unread crimson its only hues.
   The Projects header carries the Projects' one mark in the rows' mark column; its chevron follows its word. */
#nav-projects-toggle > .nav-row-label { flex: 0 1 auto; }

.nav-row-meta {
    flex: 0 0 auto;
    color: var(--text-meta);
    font-size: var(--type-meta);
}

/* The chosen place: the crimson tone, its ink and a left mark (--place-mark: the accent on the light tint, the
   fill's white ink on the dark solid), for Main and a Project row alike (U005): a Project's hue stays in its mark.
   One row at a time; the Projects header is never chosen (a Project row is). */
.nav-row.active::before {
    content: '';
    position: absolute;
    left: 3px;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 17px;
    border-radius: 0 3px 3px 0;
    background: var(--place-mark);
}

/* Unread counts and badges are the crimson accent (a live fact for you), never red and never a glow (owner Q6=A);
   a Project row's own new messages are the crimson hue of its status mark. */
/* Main's count stands in its row just before the mark's slot: the label gives way to it (and ellipsizes), it never
   covers the mark or the label, and it keeps its place when the mark comes and goes (the slot stays). */
.unread-badge {
    order: 1;
    flex: 0 0 auto;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: var(--pill-radius);
    background: var(--tone-crimson);
    color: var(--tone-crimson-ink);
    font-size: var(--type-meta);
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}

.nav-project-row { position: relative; }

/* Main's row ends where a Project row's mark column ends (the ⋯ column and the row's inset), so its room's mark sits
   at the Project rows' x by construction, in the same 18px box; its words are for readers only. */
.nav-row-main {
    padding-right: calc(var(--nav-kebab-w) + var(--space-1));
}
.nav-row-main > .nav-mark-slot {
    order: 2;
    flex: 0 0 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;  /* a one-dot shape sits where a Project row's does, mid-column */
}
.nav-row-words {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

@media (hover: hover) {
    .nav-row:hover,
    .nav-section-toggle:hover {
        background: var(--tone-quiet-hover);
        border-color: transparent;
        color: var(--text-primary);
    }
}
.nav-row.active {
    background: var(--tone-crimson);
    border-color: transparent;
    color: var(--tone-crimson-ink);
}

.nav-row:focus,
.nav-section-toggle:focus,
.app-tab:focus,
.chat-header-btn:focus {
    outline: none;
}

/* Inset: these sit flush inside the sidebar / header strip, which clips an
   outer ring (docs/DESIGN.md "Focus"). */
.nav-row:focus-visible,
.nav-section-toggle:focus-visible,
.app-tab:focus-visible,
.chat-header-btn:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}

.chat-live-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
    padding: 0 20px 16px;
}

/* In the card's foot the controls are the action row's right end: [Turn into project] [Resume] [Stop…]; the foot
   (below) carries the card's insets. */
.chat-live-foot-end > .chat-live-actions {
    flex: 0 1 auto;
    margin-inline-start: auto;
    padding: 0;
}

/* S3 (Q2/HQ1): the three-action task stop/hurry dropdown (Chat + Activity). */
.task-control-menu {
    position: fixed;
    top: var(--tcm-top, 0);
    left: var(--tcm-left, 0);
    z-index: 120;
    width: min(220px, calc(100vw - 16px));
    max-height: var(--tcm-max-height, calc(100vh - 16px));
    overflow-y: auto;
    padding: var(--space-2);
    border: 1px solid transparent;
    border-radius: var(--radius-object);
    /* The popups' opaque backing on the float shadow: a menu over a card's footer must not let its text read through. */
    background: var(--popup-float);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    box-shadow: var(--elev-float);
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.task-control-item {
    border: none;
    border-radius: var(--radius-inner);
    background: transparent;
    /* Each item is an action the owner can take: legible at rest, on the body size. */
    color: var(--text-primary);
    font: inherit;
    font-size: var(--type-body);
    line-height: var(--line-body);
    text-align: left;
    padding: var(--space-2) var(--space-3);
    cursor: pointer;
}

.task-control-item:hover:not(:disabled) {
    color: var(--text-primary);
    background: var(--menu-item-hover);
}

.task-control-item.danger {
    color: var(--status-error-fg);
}

.task-control-item:disabled {
    opacity: 0.6;
    cursor: default;
}

/* Layout-only composition for the row under a question, a Project lifecycle row or a
   routed message, with room for keyboard focus above and below. What sits in it is a
   reference (`project_reference.js`) or the shared `.btn.btn-default.btn-sm` verb. */
.system-message-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    padding-block: var(--space-3);
}

/* The layered reference tint keeps the shared colour transition. */
@property --project-reference-fill { syntax: '<color>'; inherits: true; initial-value: transparent; }
/* The Project reference (project_reference.js; docs/DESIGN.md "References and actions"): the Projects glyph, the
   Project's name and ↗ in the Project's ink on the Project's fill, in two forms only. The whole control is the
   target: hover deepens its fill (a hovering pointer only), a press deepens it again, focus wears the shared ring;
   no outline, no ring round ↗ and no second fill behind the glyph. */
.chat-live-project-card-btn {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    border: 0;
    --project-reference-fill: var(--project-08);
    background: linear-gradient(var(--project-reference-fill), var(--project-reference-fill)), var(--project-reference-backing, transparent);
    color: var(--project);
    font: inherit;
    font-size: var(--type-body);
    line-height: var(--line-body);
    text-align: left;
    cursor: pointer;
    transition: var(--motion-tone), --project-reference-fill 0.12s ease;
}
@media (hover: hover) {
    .chat-live-project-card-btn:hover { --project-reference-fill: var(--project-12); }
}
.chat-live-project-card-btn:active { filter: var(--press); }
.chat-live-project-icon { display: inline-flex; flex: none; line-height: 1; }
.chat-live-project-icon > svg { width: 1em; height: 1em; }
.chat-live-project-name { min-width: 0; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-live-project-status { flex: none; }

/* Inline, it is ONE capsule wherever content points at a Project: under a System row or a mirrored answer
   (`createSystemMessageActions`), at the end of a routed message's receipt line, in a mirrored question's voice line
   before the question and its options. It stands on the rest shadow like every pressable capsule and takes no width
   of its own: a long name ellipsizes inside the row it is given, its title and accessible name stay whole. */
.chat-quiz-project.chat-live-project-card-btn {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 100%;
    /* A row's control height and the meta size, as the footer form and a question's voice line have it (U011); a
       finger keeps the touch minimum through .ui-hit-area. */
    min-height: var(--control-h-sm);
    padding: 0 var(--space-3);
    gap: 6px;
    border-radius: var(--pill-radius);
    box-shadow: var(--elev-rest);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.chat-quiz-project.chat-live-project-card-btn:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }

/* A question's voice line (chat_media.bubbleFrameNode `voice`; docs/DESIGN.md "Quiz card"): `Ouroboros` and, in a
   Main copy, the Project it comes from, one 28px row above the card's own head. The capsule takes the row's height
   at the line's size and its free width, the name ellipsizing first; a finger keeps the touch minimum through
   `.ui-hit-area`. Neither edge line widens the bubble: the card under them decides its width. */
.chat-bubble-voice {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    min-height: var(--quiz-row-height);
    margin-bottom: var(--space-2);
    contain: inline-size;
}
/* Under a finger the capsule and the head's × each reach past their rows to the touch minimum, more than the
   --space-2 between the rows holds. The line keeps a finger's width free at its end, over the ×, so a low tap on ↗
   never lands on Close, both targets stay whole and the rows keep their height. */
@media (pointer: coarse) {
    .chat-bubble-voice:has(> .ui-hit-area) { padding-inline-end: var(--touch-target-min); }
}
.chat-bubble > .chat-bubble-voice > .sender { flex: none; margin: 0; }

/* The footer form: a work portal (work_portal.js) leads its one action row with the capsule, its controls at the
   row's end (owner 26.10, B). It takes that row's 28px control height and the meta size, as a question's voice line
   does; a finger keeps the touch minimum through `.ui-hit-area`, and a long name ellipsizes before a control gives
   way. With no control beside it (a settled work) it stays at the row's start. */
.chat-live-bound-pointer {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 100%;
    min-height: var(--control-h-sm);
    margin-inline-end: auto;
    padding: 0 var(--space-3);
    gap: 6px;
    border-radius: var(--pill-radius);
    box-shadow: var(--elev-rest);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.chat-live-bound-pointer:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
:is(.chat-quiz-project, .chat-live-bound-pointer) .chat-live-project-name { font-weight: 600; }

/* A Project root's final answer in Main is an ordinary Ouroboros message (docs/DESIGN.md
   "Project completion mirror"): folded when it is long, with the Project chip under it. The
   seven-line footprint stays fixed while the painted tail borrows footer whitespace. JS
   compensates the actual extension (including answers shorter than the tail); full text stays selectable. */
.chat-bubble.project-answer {
    --project-reference-backing: var(--voice-ouroboros);
    --project-answer-line: calc(var(--type-section) * var(--line-body));
    --project-answer-lines: 7;
    --project-answer-fold: calc(var(--project-answer-line) * var(--project-answer-lines));
    --project-answer-tail: calc(var(--project-answer-line) * 2);
}
.chat-bubble.project-answer > .message { max-height: var(--project-answer-fold); overflow: hidden; }
.chat-bubble.project-answer.is-folded > .message {
    max-height: calc(var(--project-answer-fold) + var(--project-answer-tail));
    margin-bottom: var(--project-answer-compensation, 0px);
    -webkit-mask-image: linear-gradient(to bottom, black 0, black calc(var(--project-answer-line) * 25 / 6), rgba(0,0,0,.94) calc(var(--project-answer-line) * 31 / 6), rgba(0,0,0,.65) calc(var(--project-answer-line) * 6), rgba(0,0,0,.25) var(--project-answer-fold), rgba(0,0,0,.07) calc(var(--project-answer-line) * 95 / 12), transparent calc(var(--project-answer-line) * 9));
    mask-image: linear-gradient(to bottom, black 0, black calc(var(--project-answer-line) * 25 / 6), rgba(0,0,0,.94) calc(var(--project-answer-line) * 31 / 6), rgba(0,0,0,.65) calc(var(--project-answer-line) * 6), rgba(0,0,0,.25) var(--project-answer-fold), rgba(0,0,0,.07) calc(var(--project-answer-line) * 95 / 12), transparent calc(var(--project-answer-line) * 9));
}
/* Width changes defer measurement: retain the CSS-only footprint until the feed settles. */
[data-resizing] .chat-bubble.project-answer.is-folded > .message {
    max-height: var(--project-answer-fold);
    margin-bottom: 0;
    -webkit-mask-image: linear-gradient(180deg, black 60%, transparent 100%);
    mask-image: linear-gradient(180deg, black 60%, transparent 100%);
}
/* A limitation and its disclosure own the space before Project; never paint over their words. */
.chat-bubble.project-answer:has(> .project-answer-limitation, > .project-ending-details) { --project-answer-tail: 0px; }
.chat-bubble.project-answer.is-folded > .message:has(~ .project-answer-limitation, ~ .project-ending-details) {
    -webkit-mask-image: linear-gradient(180deg, black 60%, transparent 100%);
    mask-image: linear-gradient(180deg, black 60%, transparent 100%);
}
.chat-bubble.project-answer > :is(.system-message-actions, .msg-time) { position: relative; z-index: 1; }
.chat-bubble.project-answer > .message > :first-child { margin-top: 0; }
.chat-bubble.project-answer > .system-message-actions,
.chat-bubble.assistant > .system-message-actions { padding-block: var(--space-2) var(--space-1); }
.project-answer-limitation { color: var(--text-meta); white-space: normal; overflow-wrap: anywhere; }
/* A Project ending with no authored answer (project_answer.js paintEnding): the work's name and true outcome, then its
   cause, recorded details and labelled preserved output, in the System voice at reading size; the Project's name is the
   reference under it. The same details follow an answer's limitation that names an undescribed code. */
.chat-bubble.system > .message.project-ending {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
    white-space: normal;
    overflow-wrap: anywhere;
}
.project-ending-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--space-2); min-width: 0; }
.project-ending-title { min-width: 0; font-weight: 500; }
.project-ending-cause, .project-ending-salvage { margin: 0; }
.project-ending-salvage { color: var(--text-meta); }
.project-ending-details, .chat-episode-codes { color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); overflow-wrap: anywhere; }
.project-answer-limitation + .project-ending-details { margin-top: var(--space-1); }
.project-ending-details > summary { width: fit-content; min-height: var(--touch-target-min); align-content: center; cursor: pointer; }
.project-ending-details > summary:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
.project-ending-details > p { margin: var(--space-1) 0 0; }
/* Main's questions and owner actions (docs/DESIGN.md "Project question mirror"). The waiting
   pill is one of the composer's pills (Swarm / Context / Effort): their row, height and type, at
   the row's end, amber for "waits for you"; a narrow row keeps the glyph and the number. */
.chat-question-nav {
    order: 9;
    margin-inline-start: auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: var(--composer-control-height);
    padding: 0 12px;
    border: 1px solid transparent;
    border-radius: 999px;
    background: var(--tone-amber);
    color: var(--status-warn-fg);
    box-shadow: var(--elev-rest);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    font: inherit;
    font-size: var(--composer-control-font-size);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    cursor: pointer;
    transition: var(--motion-tone);
}
.chat-question-nav:hover { background: var(--tone-amber-hover); }
.chat-question-nav:not(:disabled):active { filter: var(--press); }
.chat-question-nav[hidden] { display: none; }
.chat-question-nav:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
.chat-question-nav-arrow.down { transform: rotate(180deg); }
.chat-toolbar-row[data-owner-solo] .chat-question-nav { display: none; }
@container composer (max-width: 460px) {
    .chat-question-nav { padding: 0 10px; gap: 4px; }
    .chat-question-nav-word, .chat-question-nav-arrow { display: none; }
}
/* A quiz card's head ends, while it takes an answer, with one quiet × that closes it, as tall as the head's row; a
   finger's target is the shared `.ui-hit-area`. */
.chat-quiz-head-end { flex: none; margin-inline-start: auto; display: inline-flex; align-items: center; }
.chat-quiz-close {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--quiz-row-height);
    height: var(--quiz-row-height);
    padding: 0;
    border: 1px solid transparent;
    border-radius: 50%;
    background: transparent;
    color: var(--text-meta);
    cursor: pointer;
}
.chat-quiz-close:hover:not(:disabled) { color: var(--text-primary); background: var(--tone-quiet-hover); }
.chat-quiz-close:disabled { color: var(--text-disabled); cursor: default; }
.chat-quiz-close:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
/* An owner-required action is a System message: the task, one sentence, the task's own controls. */
.owner-action-body { display: flex; flex-direction: column; gap: var(--space-2); }
.owner-action-reason { font-size: var(--type-meta); color: var(--text-meta); }
.chat-quiz-materials { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-block: var(--space-2); }


/* A block that exists only for open attention (a model wait, a pending or
   host-offered Stop) or a bare non-Done ending carries no title placeholder
   (docs/DESIGN.md "Conversation activity block"); its chip says the state it is
   in (Waiting…, Cancelling…, Failed). The first row of work — a tool call,
   narration, a child, a review, an error — gives it a title, whatever lane ran it. */
.chat-live-card > .chat-live-summary-button .chat-live-title:empty {
    display: none;
}

.chat-live-actions:empty {
    display: none;
}

.nav-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin-top: var(--space-2);
}

.nav-chevron {
    flex: 0 0 auto;
    color: var(--text-muted);
    transition: transform 0.15s ease;
}

/* Main content in the shell grid. */
#content {
    grid-column: 2;
    width: 100%;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    min-width: 0;
    position: relative;
    z-index: 1;
}

.page {
    display: none;
    flex-direction: column;
    height: 100%;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    position: relative;
}
.page.active { display: flex; }

/* The room is plain (DESIGN.md design bible): no wash, glow or texture behind the page. */
.app-page-glass {
    background: none;
}

/* Containment for narrow screens and unbroken runtime text. */
#content,
#content .page,
#chat-messages,
.chat-messages,
#log-entries,
.log-entry,
.log-main,
.log-body,
.log-headline,
.log-task-summary,
.log-task-timeline,
.chat-bubble,
.chat-bubble .message,
.marketplace-card,
.skills-card,
.widgets-list,
.widgets-card {
    min-width: 0;
}

.log-body,
.log-headline,
.log-pill,
.log-raw,
.chat-bubble .message,
.skills-card,
.widgets-card {
    overflow-wrap: anywhere;
}

/* The list's column is 280–360px while the page has room for it beside a file, and never wider than the page itself:
   below 700px one pane takes the whole page (the `files` container query below), so a floor above the page's own width
   only pushes that pane's header, location and actions out past the column that holds them (DESIGN "Page header":
   folder names fit and actions wrap). */
.files-layout {
    position: relative;
    display: grid;
    grid-template-columns: minmax(min(280px, 100%), 360px) minmax(0, 1fr);
    grid-template-areas: "sidebar preview";
    flex: 1;
    height: auto;
    min-height: 0;
    overflow: hidden;
}

/* Page-body controls take the touch reach of their family (ui.css): no 44px body on a phone. Links stay inline and
   wrap before they widen a column; under a finger they take the hit extension's inset (ui.css --ui-hit-inset), so a
   link inside a sentence keeps its lines. */
:is(.files-layout, .skills-scroll, .skills-search-chrome, .widgets-scroll, .dashboard-shell, .settings-shell) a[href] {
    position: relative;
    overflow-wrap: anywhere;
}
@media (pointer: coarse) {
    :is(.files-layout, .skills-scroll, .skills-search-chrome, .widgets-scroll, .dashboard-shell, .settings-shell) a[href]::after {
        content: "";
        position: absolute;
        inset: var(--ui-hit-inset);
    }
}

.files-sidebar {
    grid-area: sidebar;
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    border-right: 1px solid var(--divider);
    background: rgba(var(--neutral-rgb), 0.02);
}

.files-toolbar {
    display: flex;
    gap: 8px;
    padding: 12px 16px 10px;
    border-bottom: 1px solid rgba(var(--neutral-rgb), 0.04);
    flex-shrink: 0;
}

.files-toolbar input { flex: 1; min-width: 0; }

.files-browser-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    padding: 12px 16px;
    border-bottom: 1px solid rgba(var(--neutral-rgb), 0.04);
    flex-shrink: 0;
}

.files-breadcrumb {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    flex: 1 1 280px;
    min-width: 0;
    overflow: hidden;
}

.files-browser-actions {
    display: inline-flex;
    gap: 8px;
    flex-shrink: 0;
    margin-left: auto;
}

.files-browser-header > .files-browser-actions { flex-wrap: wrap; flex-shrink: 1; min-width: 0; }
/* On a phone (the stack layout) the location shares its row with its actions: its folders wrap beside them. */
html[data-layout="stack"] .files-browser-header > .files-breadcrumb { flex-basis: 0; }

.files-crumb {
    border: none;
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
    font-size: 12px;
    max-width: min(360px, 100%);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.files-crumb:hover {
    color: var(--text-primary);
}

/* The location's folders wrap on a phone inside a row that clips (overflow: hidden), so under a finger each folder
   takes the touch minimum as its own height rather than a reach the row would cut. */
@media (pointer: coarse) {
    .files-breadcrumb { align-items: center; }
    .files-crumb { min-height: var(--touch-target-min); }
}

.files-crumb-sep {
    color: var(--text-muted);
    font-size: 12px;
}

.files-list {
    flex: 1;
    min-height: 0;
    overflow: auto;
    overscroll-behavior: contain;
    padding: 8px;
}

.files-entry {
    width: 100%;
    display: grid;
    grid-template-columns: 20px minmax(0, 1fr) auto 16px;
    gap: 10px;
    align-items: center;
    border: none;
    background: transparent;
    color: var(--text-primary);
    border-radius: var(--radius-inner);
    padding: 10px 12px;
    cursor: pointer;
    text-align: left;
    /* A row is a control: pressing or right-clicking it never selects its name (the preview keeps selection). */
    -webkit-user-select: none;
    user-select: none;
    /* Rows touch, so no reach can stand out past them: a row is the touch minimum tall (44px under a finger). */
    min-height: var(--touch-target-min);
}

.files-entry:hover {
    background: rgba(var(--neutral-rgb), 0.04);
}

.files-entry.parent-link {
    opacity: 0.88;
}

/* The selected file keeps its own ink on the accent's faint wash, with no ring (DESIGN principle 4). */
.files-entry.selected { background: var(--accent-10); }

.files-entry-icon,
.files-entry-chevron { display: inline-flex; align-items: center; justify-content: center; color: var(--text-secondary); }
.files-entry-chevron { color: var(--text-muted); }
.files-entry-icon svg { width: 16px; height: 16px; }
.files-entry-chevron svg { width: 14px; height: 14px; }

.files-entry-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.files-entry-meta {
    color: var(--text-muted);
    font-size: var(--type-meta);
}

.files-empty {
    padding: 18px 12px;
    color: var(--text-muted);
    font-size: var(--type-body);
}

.files-preview {
    grid-area: preview;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    background: rgba(var(--neutral-rgb), 0.015);
}

.files-preview-header {
    flex-wrap: wrap;
    padding: 16px 20px 12px;
    border-bottom: 1px solid var(--divider);
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    flex-shrink: 0;
}

.files-preview-header[hidden] { display: none; }
.files-preview-header > :first-child { flex: 1 1 220px; min-width: 0; }
.files-preview-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    min-width: 0;
}
.files-preview-status { padding: var(--space-2) var(--space-4); overflow-wrap: anywhere; }

.files-preview-path {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
    overflow-wrap: anywhere;
}

.files-preview-meta {
    margin-top: 4px;
    font-size: 12px;
    color: var(--text-secondary);
}

.files-preview-content {
    flex: 1;
    min-height: 0;
    overflow: auto;
    overflow-wrap: anywhere;
    overscroll-behavior: contain;
    margin: 0;
    padding: 20px;
    color: #d9dee8;
    font-size: 12px;
    line-height: 1.55;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
    white-space: pre-wrap;
    word-break: break-word;
}
/* The preview's own sentence (nothing chosen, no preview for this kind of file) reads in the system face. */
.files-preview-content > .files-empty { padding: 0; font-family: var(--font-sans); white-space: normal; }

.files-editor-shell {
    display: flex;
    flex-direction: column;
    min-height: 100%;
    gap: 12px;
}

.files-editor {
    flex: 1;
    min-height: 320px;
    resize: none;
    margin: 0;
}

.files-preview-image {
    display: block;
    max-width: 100%;
    max-height: 100%;
    margin: 0 auto;
    border-radius: var(--radius-inner);
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
}

.files-preview-frame {
    width: 100%;
    min-height: 70vh;
    border: 1px solid var(--divider);
    border-radius: var(--radius-inner);
    background: #111827;
}

.files-drop-overlay {
    position: absolute;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(var(--menu-rgb), 0.72);
    backdrop-filter: blur(3px);
    z-index: 5;
    pointer-events: none;
}

.files-layout.drag-active .files-drop-overlay {
    display: flex;
}

.files-drop-card {
    padding: 22px 28px;
    border-radius: var(--radius-object);
    border: 1px dashed var(--accent-55);
    background: var(--accent-10);
    color: var(--text-primary);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.01em;
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.24);
}

.files-modal-input { margin-top: var(--space-3); }

/* The folder's list and the chosen file side by side while the page has room for the list's widest column and a
   working file beside it (360 + 340); a narrower page (a phone, a narrow window) shows the list first and the file
   once one is chosen, with Back to folder above it (files.js). Only there does a row say its edits are unsaved and
   a new file's draft keep a row of its own, the two ways back to a draft. */
.files-layout { container: files / inline-size; }
.files-preview-back,
.files-entry.is-draft,
.files-entry-unsaved { display: none; }
.files-back svg { display: block; width: 16px; height: 16px; }
.files-entry-untitled {
    min-width: 0;
    overflow: hidden;
    color: var(--text-primary);
    text-overflow: ellipsis;
    white-space: nowrap;
}

@container files (max-width: 700px) {
    .files-sidebar,
    .files-preview { grid-column: 1 / -1; }
    .files-sidebar { border-right: 0; }
    .files-layout:not([data-files-view="file"]) .files-preview,
    .files-layout[data-files-view="file"] .files-sidebar { display: none; }
    .files-preview-back { display: flex; padding: var(--space-3) 20px 0; }
    .files-entry.is-draft { display: grid; }
    .files-entry:is(.is-draft, [data-unsaved]) .files-entry-unsaved { display: inline; color: var(--status-warn-fg); }
    .files-entry[data-unsaved] .files-entry-size { display: none; }
}

/* The one header (page_header.js; DESIGN "Shell"): one row, `☰ · title · status … actions · ×`. The leading
   slot holds ☰ (shown by the stack layout), the title row the place's name and the room's status, the toolbar the
   actions, the trailing slot a Project's ×. On a desk a page's tabs or location take the hidden title's place in
   the row (`.app-page-lead`); in the stack layout they are their own strip under it. A page has no caption line. */
.page-header,
.app-page-header {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "leading title toolbar";
    align-items: center;
    gap: var(--space-2) var(--space-3);
    padding: var(--page-header-padding-y) var(--page-header-padding-x);
    background: transparent;
    backdrop-filter: none;
    flex-shrink: 0;
    border-bottom: none;
}

.app-page-leading {
    grid-area: leading;
    display: flex;
    align-items: center;
    min-width: 0;
}
.app-page-leading:empty { display: none; }

.app-page-title-block {
    grid-area: title;
    min-width: 0;
}

.app-page-title-row {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

/* One title style: the section size at 500 (a root's title), the primary ink, one line. */
.page-header h2,
.app-page-title {
    margin: 0;
    min-width: 0;
    font-size: var(--type-section);
    font-weight: 500;
    line-height: var(--line-title);
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Where the sidebar's chosen row already names the place, a desk shows no title (the h2 stays for readers), and
   a header with nothing else to say takes no room (owner Q3=A). A Project keeps its name in every layout. */
html[data-layout="docked"] .app-page-header[data-sidebar-names] .app-page-title {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
html[data-layout="docked"] .app-page-header[data-empty] {
    display: none;
}
/* In the macOS desktop window the band under the transparent title bar is the window's handle
   (launcher_window_chrome.py: a plain press there moves the window, a press on a control stays the page's). The band
   is the window's, not a row of the page: a header with nothing to show lies over the top of its page as that handle
   (clear of the scrollbar, as a room's strip is), the page's first row starts below the band, and scrolled content
   passes under it with crisp edges, as a room's feed does. */
:root[data-window-chrome="integrated"][data-layout="docked"] .app-page-header[data-empty] {
    display: block;
    position: absolute;
    inset: 0 6px auto 0;
    z-index: 2;
    height: var(--window-chrome-top);
    padding: 0;
}
:root[data-window-chrome="integrated"][data-layout="docked"] .app-page-header[data-empty] ~ .widgets-scroll {
    padding-top: calc(var(--space-4) + var(--window-chrome-top));
}
/* While a card is dragged the band lets the pointer through to the board, whose top edge is where the engine scrolls
   a drag upward; at rest the band is the window's handle. */
:root[data-window-chrome="integrated"][data-layout="docked"] #page-widgets .app-page-header[data-empty]:has(~ .widgets-scroll .widgets-card.dragging) {
    pointer-events: none;
}

.app-page-toolbar,
.app-page-actions,
.app-page-trailing {
    grid-area: toolbar;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    min-width: 0;
    justify-self: end;
}

.app-page-toolbar {
    max-width: 100%;
}

.app-page-trailing {
    max-width: 100%;
}

/* Under a finger every header action is one of the header's controls: the chrome height, which ui.css raises to
   the touch minimum on a coarse pointer (Refresh, a Project's ×, the bars' icon controls alike). */
@media (pointer: coarse) {
    .app-page-toolbar { --control-h: var(--control-h-float); }
    .app-page-toolbar button,
    .app-page-trailing button {
        min-height: var(--control-h-float);
    }
}

/* In the stack layout the tab strip is its own strip under the header row (it scrolls sideways by design). */
.app-page-tabs {
    flex: 0 0 auto;
    min-width: 0;
    padding: 0 var(--page-header-padding-x) var(--page-header-padding-bottom);
}

/* A page's own navigation in its bar (page_header.js bindPageHeader; DESIGN "Page header", owner 19.10 A). On a desk
   the bar of a page the sidebar names has two ends, as Main's has: its start holds the page's tabs or Files' location
   where the hidden title stood, its end the page's actions, every control at the chrome height on Main's line (in
   the macOS window, centred on the band under the transparent title bar, whose empty parts still move the window).
   The strip keeps its sideways scroll, fades and revealed choice, and lends its tabs' shadows room below the bar
   without taking presses there. The stack layout keeps the nodes under the row. */
.app-page-lead { display: flex; align-items: center; min-width: 0; }
.app-page-lead > * { min-width: 0; }
.app-page-lead:empty,
.app-page-tabs:not(:has(> *)) { display: none; }
/* A narrow content column gives the existing toolbar its own wrapping row; navigation lives below it. */
html[data-layout="docked"] .app-page-header[data-narrow] { grid-template-columns: minmax(0, 1fr); grid-template-areas: "toolbar"; }
html[data-layout="docked"] .app-page-header[data-narrow] > .app-page-leading { display: none; }
html[data-layout="docked"] .app-page-header[data-narrow] > .app-page-title-block { position: absolute; }
html[data-layout="docked"] .app-page-header[data-narrow] > .app-page-toolbar { flex-wrap: wrap; }
/* A narrow header's actions wrap a long label instead of leaving the column (a translation, or a wider platform font:
   "Update 2 trusted" in a 160 px column on Linux); like the Evolution verbs, the box comes from the tokens; a fixed
   16 px line keeps one line at the small capsule's 28 px (1 + 5 + 16 + 5 + 1) on every platform font. */
html[data-layout="docked"] .app-page-header[data-narrow] > .app-page-toolbar > button { flex: none; max-width: 100%;
    white-space: normal; overflow-wrap: anywhere; line-height: 16px; --control-h: auto; --control-pad-y: 5px; }
html[data-layout="docked"] .app-page-header[data-lead] {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "title toolbar";
    min-height: 50px;
    padding-block: 0;
}
/* In the macOS window the row is the window's band, so its tabs and actions sit level with the traffic lights at every
   Interface size: a larger size shrinks the band in CSS px, and the strip's insets shrink with it, the bottom one
   keeping the tabs' 16px of shadow room (at 100 % they are the 10px and 26px above; once the band is shorter than a
   control the row grows below the band). */
:root[data-window-chrome="integrated"][data-layout="docked"] .app-page-header[data-lead] {
    --lead-inset: min(10px, max(0px, calc((var(--window-chrome-top) - var(--control-h-float)) / 2)));
    min-height: var(--window-chrome-top);
}
:root[data-window-chrome="integrated"][data-layout="docked"] .app-page-lead .app-tab-strip {
    padding-top: var(--lead-inset);
    padding-bottom: calc(16px + var(--lead-inset));
}
html[data-layout="docked"] .app-page-header[data-lead] > .app-page-leading { display: none; }
html[data-layout="docked"] .app-page-header[data-lead] > .app-page-toolbar { --control-h: var(--control-h-float); }
html[data-layout="docked"] .app-page-header[data-lead] > .app-page-toolbar button {
    height: var(--control-h-float);
    min-height: var(--control-h-float);
    padding-block: 0;
}
html[data-layout="docked"] .app-page-lead .app-tab-strip {
    flex: 1 1 auto;
    padding: 10px var(--space-1) 26px;
    margin: 0 0 -16px calc(-1 * var(--space-1));
    scroll-padding-inline: var(--space-1);
    pointer-events: none;
}
html[data-layout="docked"] .app-page-lead { --control-h: var(--control-h-float); }
html[data-layout="docked"] .app-page-lead .app-tab { pointer-events: auto; }
/* Files: the folder's location starts the bar at reading size, each folder by its name, the one you are in in primary
   ink. A location longer than the bar's start scrolls inside it as a tab strip does, fading where folders hide: it
   rests at its end (files.js), so the folders above give way first, and no folder is wider than the strip less one
   fade, so the one you are in rests clear of it (a longer name ellipsizes; its words stay its accessible name). Its
   own actions (Paste, + File, + Folder) join Refresh at the bar's end. */
html[data-layout="docked"] .app-page-lead > .files-breadcrumb {
    flex: 1 1 auto;
    flex-wrap: nowrap;
    align-items: center;
    gap: 2px;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-padding-inline: var(--space-4);
    scrollbar-width: none;
    --crumb-fade-start: 0px;
    --crumb-fade-end: 0px;
    -webkit-mask-image: linear-gradient(to right, transparent, black var(--crumb-fade-start),
        black calc(100% - var(--crumb-fade-end)), transparent);
    mask-image: linear-gradient(to right, transparent, black var(--crumb-fade-start),
        black calc(100% - var(--crumb-fade-end)), transparent);
}
html[data-layout="docked"] .app-page-lead > .files-breadcrumb::-webkit-scrollbar { display: none; }
html[data-layout="docked"] .app-page-lead > .files-breadcrumb[data-scroll-start] { --crumb-fade-start: var(--space-4); }
html[data-layout="docked"] .app-page-lead > .files-breadcrumb[data-scroll-end] { --crumb-fade-end: var(--space-4); }
html[data-layout="docked"] .app-page-lead .files-crumb {
    flex: none;
    max-width: min(360px, calc(100% - var(--space-4)));
    min-height: var(--control-h-float);
    padding: 0 var(--space-1);
    font-size: var(--type-body);
    font-weight: 500;
}
/* Inset: the strip clips an outer ring (DESIGN "Focus"). */
html[data-layout="docked"] .app-page-lead .files-crumb:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: -2px; }
html[data-layout="docked"] .app-page-lead .files-crumb:last-of-type { color: var(--text-primary); }
html[data-layout="docked"] .app-page-lead .files-crumb-sep { flex: none; font-size: var(--type-body); }
.files-browser-header:not(:has(> *)) { display: none; }

.app-tab-strip {
    display: flex;
    flex-wrap: nowrap;
    gap: 10px;
    overflow-x: auto;
    min-width: 0;
    max-width: 100%;
    /* The strip scrolls sideways, so it lends the capsules' shadow room inside its own box instead of clipping it,
       and fades its clipped ends (scroll_fade.js sets data-scroll-start / data-scroll-end). */
    padding: var(--space-2) var(--space-4) var(--space-5);
    scroll-padding-inline: var(--space-4);
    margin: 0;
    scrollbar-width: none;
    --tab-fade-start: 0px;
    --tab-fade-end: 0px;
    -webkit-mask-image: linear-gradient(to right, transparent, black var(--tab-fade-start),
        black calc(100% - var(--tab-fade-end)), transparent);
    mask-image: linear-gradient(to right, transparent, black var(--tab-fade-start),
        black calc(100% - var(--tab-fade-end)), transparent);
}
.app-tab-strip[data-scroll-start] { --tab-fade-start: var(--space-4); }
.app-tab-strip[data-scroll-end] { --tab-fade-end: var(--space-4); }
/* A strip that scrolls clips a reach, so under a finger its tabs take the touch minimum as their own height (the
   control token; the lock writes the box). */
@media (pointer: coarse) {
    .app-tab-strip { --control-h: var(--touch-target-min); }
}

.app-tab-strip::-webkit-scrollbar {
    display: none;
}

/* The tab itself (.app-tab, its count pill) is ui.css's; the strip above is the page's composition. */

.scroll-fade-y {
    --scroll-fade-edge: 32px;
    --scroll-fade-top: 0px;
    --scroll-fade-bottom: 0px;
    -webkit-mask-image: linear-gradient(180deg, transparent 0,
        black var(--scroll-fade-top), black calc(100% - var(--scroll-fade-bottom)), transparent 100%);
    mask-image: linear-gradient(180deg, transparent 0,
        black var(--scroll-fade-top), black calc(100% - var(--scroll-fade-bottom)), transparent 100%);
}
.scroll-fade-y[data-scroll-above] { --scroll-fade-top: var(--scroll-fade-edge); }
.scroll-fade-y[data-scroll-below] { --scroll-fade-bottom: var(--scroll-fade-edge); }

.page-header .spacer { flex: 1; }

.app-page-header-overlay {
    align-items: center;
}


/* Main's header is one bar like a Project's, one row in every layout (DESIGN "Page header"): the title with the
   room's status right after it, then the agent's commands. The commands keep their width and the title block
   yields to them, its status word dropping whole while the mark stays (room_work.js); the consciousness chip follows
   the title block and keeps its width; a narrow bar has Restart and the budget inside the status Shade (page_header.js).
   A wider face (Linux sets these words wider) moves nothing to a second row. */
.chat-page-header {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: var(--z-page-header);
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: space-between;
    column-gap: var(--space-3);
    pointer-events: none;
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    -webkit-mask-image: none;
    mask-image: none;
    overflow: visible;
}
.chat-page-header > .app-page-leading { flex: 0 0 auto; }
.chat-page-header > .app-page-title-block { flex: 0 1 auto; min-width: 0; }
.chat-page-header > .app-page-actions { flex: 0 0 auto; justify-content: flex-end; }
/* A room's bar (Main's and a Project's) ends in one group of capsules: the consciousness chip leads it
   (chat_header.js), then Main's Restart, Panic and budget or a Project's Panic and ×, all one gap apart (owner seed S3);
   Main's commands lie in the group as its own capsules. */
:is(.chat-page-header, .project-panel-bar) > :is(.app-page-actions, .app-page-trailing) { gap: var(--space-2); }
.chat-header-actions { display: contents; }
/* On a phone, while a Project's one-line name is still cut, the chip shows its brain and mark alone (room_work.js
   fitWord); its name says it all. */
.project-panel-bar > .app-page-trailing > .cx-chip[data-fit="mark"] .cx-chip-word { display: none; }

.chat-page-header > * {
    pointer-events: auto;
}

/* The history note (a reading-window fact: a failed read, an approximate position) is its own floating chip under
   the bar, never a full-width strip: it floats at the bar's measured height (chat.js writes --chat-header-bar)
   with its own float-sheet backing, and the transcript's reserve counts both. Empty, it takes no room. */
.chat-page-note {
    position: absolute;
    top: var(--chat-header-bar, 56px);
    left: var(--page-header-padding-x);
    right: auto;
    max-width: min(560px, calc(100% - 2 * var(--page-header-padding-x)));
    z-index: var(--z-page-header);
    padding: 2px 0 var(--space-2);
    pointer-events: none;
}
.chat-page-note > * {
    display: block;
    padding: 6px 12px;
    border-radius: var(--radius-inner);
    background: var(--surface-float);
    box-shadow: var(--elev-rest);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    pointer-events: auto;
}
.chat-page-note:not(:has(> :not([hidden]))) { display: none; }

/* The bars' controls (DESIGN.md design bible): neutral capsules on glass, standing on the rest shadow; Panic is the
   danger tone (--panic-fill: on dark the capsule's own veil, since the red tone there is already a neutral veil and
   stacking it washes out Panic's ink), the budget neutral and amber only while a spending limit's stop waits for the
   owner. Hover deepens the fill, a press deepens it again; what cannot be pressed loses its shadow. */
.chat-header-btn {
    border: 1px solid transparent;
    background: var(--chip-float);
    color: var(--text-primary);
    box-shadow: var(--elev-rest);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    font-size: var(--type-meta);
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    transition: var(--motion-tone);
}

@media (hover: hover) {
    .chat-header-btn:hover { background: var(--chip-float-hover); box-shadow: var(--elev-rest); }
    .chat-header-btn.danger:hover { background: var(--panic-fill-hover); }
}
.chat-header-btn:not(:disabled):active { filter: var(--press); }
.chat-header-btn:disabled { box-shadow: none; }

.chat-header-btn.danger {
    background: var(--panic-fill);
    color: var(--panic-ink);
}

/* The budget: a neutral fact (spent / limit and its bar). It turns amber only while a task the spending limit
   stopped waits for the owner (data-attention, from Main's attention rows): then the money is the owner's move. */
.chat-budget-pill {
    appearance: none;
    position: relative;
    border: 1px solid transparent;
    background: var(--chip-float);
    box-shadow: var(--elev-rest);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    cursor: pointer;
    min-width: 100px;
    font: inherit;
    transition: var(--motion-tone);
}
.chat-budget-pill[data-attention="1"] { background: linear-gradient(var(--tone-amber), var(--tone-amber)), var(--chip-float); }

@media (hover: hover) {
    .chat-budget-pill:hover { background: var(--chip-float-hover); }
    .chat-budget-pill[data-attention="1"]:hover { background: linear-gradient(var(--tone-amber-hover), var(--tone-amber-hover)), var(--chip-float); }
}
.chat-budget-pill:not(:disabled):active { filter: var(--press); }
.chat-budget-pill:disabled { box-shadow: none; }

.chat-budget-text {
    font-size: var(--type-meta);
    font-weight: 600;
    color: var(--text-secondary);
    white-space: nowrap;
    letter-spacing: 0.02em;
}

/* The amount keeps the capsule's centre line, as Restart's and Panic's words do; its track lies along the capsule's
   bottom inset, out of the line (U004). */
.chat-budget-bar {
    position: absolute;
    inset-inline: var(--pill-padding-x);
    bottom: var(--space-1);
    height: 2px;
    border-radius: 1px;
    background: var(--status-neutral-bg);
    overflow: hidden;
}

.chat-budget-bar-fill {
    height: 100%;
    border-radius: 1px;
    background: var(--text-meta);
    transition: width 0.4s ease;
    width: var(--budget-fill, 0);
}
/* The pill names itself only where it stands as a row (the narrow bar's status Shade, room_work.css). */
.chat-budget-word { display: none; }

.chat-budget-pill[data-attention="1"] .chat-budget-text { color: var(--status-warn-fg); }
.chat-budget-pill[data-attention="1"] .chat-budget-bar { background: var(--amber-dim); }
.chat-budget-pill[data-attention="1"] .chat-budget-bar-fill { background: var(--amber); }

.status-badge {
    padding: 4px 10px;
    font-size: var(--type-meta);
    font-weight: 500;
    background: rgba(var(--composer-rgb), 0.78);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}
.status-badge.online { color: var(--status-ok-fg); }
/* Loading… and Starting… go on by themselves: the meta ink, never amber. */
.status-badge.starting { color: var(--text-meta); }
.status-badge.error { color: var(--status-error-fg); }
.status-badge.offline { color: var(--text-muted); }
.chat-page-header .status-badge,
.chat-panel-statusbar .status-badge {
    max-width: 100%;
    white-space: normal;
}
.status-badge.thinking {
    color: var(--accent);
    animation: thinking-pulse 1.5s ease-in-out infinite;
}
@keyframes thinking-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}
@media (prefers-reduced-motion: reduce) {
    .status-badge.thinking { animation: none; }
}

/* History navigation at the top edge of the rendered window. */
.chat-load-older {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: 6px 12px 10px;
}
.chat-load-older[hidden] { display: none; }
/* Navigation over the transcript: Load older rests in the reading flow; ↓ latest and Back to reading fly over it. */
.chat-load-older-btn {
    appearance: none;
    border: 1px solid transparent;
    border-radius: 999px;
    background: var(--surface-capsule);
    color: var(--text-primary);
    box-shadow: var(--elev-rest);
    font-size: var(--type-meta);
    padding: 5px 14px;
    cursor: pointer;
    transition: var(--motion-tone);
}
.chat-load-older-btn:hover:not(:disabled) {
    color: var(--text-primary);
    background: var(--surface-capsule-hover);
}
.chat-load-older-btn:not(:disabled):active { filter: var(--press); }
.chat-load-older-btn:disabled { opacity: 0.6; cursor: default; box-shadow: none; }

/* A completed Continue is a persistent reference, still readable when inert. */
.btn[data-continue-successor]:disabled {
    opacity: 1;
    color: var(--text-secondary);
    cursor: default;
}

/* First read of an empty feed: the pill is the only thing in the panel, so it
   carries the shared in-progress pulse rather than reading as a dead control. */
.chat-load-older[aria-busy="true"] .chat-load-older-btn {
    animation: thinking-pulse 1.5s ease-in-out infinite;
}
.chat-load-older-btn:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}
.msg-provenance {
    font-size: var(--type-meta);
    color: var(--text-meta);
    margin-top: var(--space-2);
}
:is(.chat-load-older-note, .chat-history-gap) {
    font-size: var(--type-meta);
    color: var(--text-meta);
    text-align: center;
    max-width: 460px;
}
.chat-history-status {
    grid-column: 1 / -1;
    flex: 0 0 100%;
    order: 1;
    max-width: none;
    text-align: left;
    overflow-wrap: anywhere;
}
/* The transcript's typing bubble: the shared moving mark (status_mark.js) in the bubble's secondary ink, on a line of
   its own height. */
.typing-bubble { opacity: 0.7; color: var(--text-secondary); }
.typing-bubble > .typing-mark { height: 20px; }


.onboarding-overlay {
    position: fixed;
    inset: 0;
    z-index: 50;
    display: flex;
    align-items: stretch;
    justify-content: center;
}

.onboarding-overlay-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(var(--media-rgb), 0.78);
    backdrop-filter: blur(10px);
}

.onboarding-surface {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
    width: 100%;
}

.onboarding-frame {
    flex: 1;
    min-height: 0;
    width: 100%;
    border: 0;
    background: transparent;
}

.onboarding-recovery {
    display: flex;
    justify-content: flex-end;
    padding: 8px;
    background: var(--bg-primary);
}

.onboarding-restart-card {
    position: relative;
    z-index: 1;
    align-self: center;
    width: min(460px, calc(100vw - 32px));
    padding: 24px;
    border: 1px solid var(--divider);
    border-radius: var(--radius-inner);
    background: var(--bg-secondary);
    color: var(--text-primary);
    box-shadow: 0 24px 80px rgba(0, 0, 0, 0.38);
}

.onboarding-restart-card h2 {
    margin: 0 0 10px;
    font-size: 20px;
}

.onboarding-restart-card p {
    margin: 0 0 18px;
    color: var(--text-secondary);
    line-height: 1.5;
}


#chat-messages,
.chat-messages {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 20px;
    padding-top: var(--chat-header-reserve, 56px);
    padding-bottom: 0;
    display: flex;
    flex-direction: column;
    gap: 14px;
    /* Container for the late live-card width override. A narrow project panel
       must respond to this column's width, not to the outer viewport. */
    container-type: inline-size;
    container-name: chatcol;
    /* One measure for every object of the left voice (U009): Ouroboros' bubbles, System rows, work and living cards
       end on one right edge — 760px at most, the 80% gutter only while 80% is still 620px or more, the whole column
       from 620px down. Resolved against each object's own column. */
    --feed-measure: min(760px, max(80%, min(100%, 620px)));
}

/* The transcript is a scrolling column, so rows must keep their natural
   height. Children with overflow:hidden (live cards, skill_review bubbles)
   otherwise get an automatic flex min-height of 0 and are shrunk to a 1px
   strip when the transcript overflows instead of letting the list scroll. */
#chat-messages > *,
.chat-messages > * {
    flex-shrink: 0;
    contain-intrinsic-size: auto 160px;
}

/* Lazy rows while the column's width keeps moving (a live window resize, the sidebar's or a Project panel's handle):
   chat_reading_position.js marks the feed [data-resizing], and the browser lays out only the rows near the screen;
   every other row keeps the size it was last laid out at (the `auto` above remembers it; 160px stands in only for a
   row never laid out). Once the width holds, the mark goes, every row is laid out at the final width and the
   reading place is re-applied in that same frame. */
:is(#chat-messages, .chat-messages)[data-resizing] > * {
    content-visibility: auto;
}

/* Rows asleep at rest (chat_row_sleep.js; owner 20.10, 3A): a bubble or work card laid out at the feed's width with
   everything in it, and more than a screen from the view, is skipped and keeps that size, its own; a row that
   changes, comes near the view, a new width, theme or fonts wake it. Only such rows: a row that takes its real size
   only as it nears the screen moves the passage being read in WebKit, as does a contained row near the view (WebKit's
   scroll anchoring then fails); landings, history pages and the bottom are measured against exact rows
   (tests/test_chat_lazy_rows_browser.py, tests/test_chat_row_sleep_browser.py). With the rows far away skipped,
   moving the room card under the drawer no longer restyles and repaints the whole feed. */
:is(#chat-messages, .chat-messages) > [data-laid] {
    content-visibility: auto;
}

/* An actual flex item keeps the overlay reserve inside scrollable overflow in
   WKWebView, including short histories. The column gap supplies the final 8px. */
#chat-messages::after,
.chat-messages::after {
    content: '';
    flex: 0 0 calc(var(--chat-input-reserve, 108px) - 8px);
}

/* Quiet transcript navigation: ↓ latest stands at the composer's end, right above the waiting pill (main_waiting.js,
   the end of the capsule row), so every way down the feed is one stack in one place and never floats over the text
   in the middle of the column (U016). */
.chat-scroll-bottom-btn {
    position: absolute;
    right: var(--space-2);
    bottom: calc(100% + var(--space-2));
    z-index: 6;
    width: var(--composer-control-height);
    height: var(--composer-control-height);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    color: var(--text-secondary);
    background: var(--surface-capsule);
    border: 1px solid transparent;
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    box-shadow: var(--elev-float);
    cursor: pointer;
    opacity: 0;
    transform: translateY(8px);
    pointer-events: none;
    transition: opacity 0.18s ease, transform 0.18s ease, border-color 0.18s ease, background 0.18s ease, color 0.18s ease;
}
.chat-scroll-bottom-btn.visible {
    opacity: 1;
    transform: none;
    pointer-events: auto;
}
.chat-scroll-bottom-btn:hover { color: var(--text-primary); }
.chat-scroll-bottom-btn:not(:disabled):hover { background: var(--surface-capsule-hover); }
.chat-scroll-bottom-btn:not(:disabled):active { filter: var(--press); }
.chat-scroll-bottom-btn:disabled { box-shadow: none; }
.chat-scroll-bottom-btn:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}
.chat-scroll-bottom-btn svg { display: block; }
.chat-scroll-activity-dot {
    position: absolute;
    top: -2px;
    right: -2px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 2px var(--bg-primary);
}
.chat-scroll-activity-dot[hidden] { display: none; }

/* Voices (DESIGN.md design bible): the owner blue, Ouroboros blush (a task card a quieter blush), System its own body
   and name, each a resting object on the rest shadow without an edge; 12 x 18 inside a bubble. */
.chat-bubble {
    padding: 12px 18px 10px;
    border-radius: var(--radius-object);
    max-width: 80%;
    word-wrap: break-word;
    overflow-wrap: anywhere;
    min-width: 0;
}

/* Host-owned empty state, never an assistant or historical chat bubble. */
.chat-empty-welcome {
    align-self: flex-start;
    max-width: min(80%, 520px);
    min-width: 0;
    padding: var(--space-3) var(--space-4);
    border: 1px solid transparent;
    border-radius: var(--radius-object);
    background: var(--surface-object);
    overflow-wrap: anywhere;
}
.chat-empty-welcome-label {
    color: var(--text-secondary);
    font-size: var(--type-meta);
    font-weight: 600;
}
.chat-empty-welcome p {
    margin: var(--space-1) 0 0;
    color: var(--text-primary);
    font-size: var(--type-section);
    white-space: pre-wrap;
}

.chat-bubble.user {
    background: var(--voice-owner);
    border: 1px solid transparent;
    box-shadow: var(--elev-rest);
    align-self: flex-end;
    margin-left: 20%;
}

.chat-bubble.assistant {
    background: var(--voice-ouroboros);
    border: 1px solid transparent;
    box-shadow: var(--elev-rest);
    align-self: flex-start;
    max-width: var(--feed-measure);
}

.chat-bubble.system {
    background: var(--system-bubble-bg);
    box-shadow: var(--elev-rest);
    align-self: flex-start;
    max-width: var(--feed-measure);
}

.chat-bubble .sender {
    font-size: var(--type-meta);
    font-weight: 600;
    margin-bottom: 4px;
}
.chat-bubble.user .sender { color: var(--user-sender); }
.chat-bubble.assistant .sender { color: var(--assistant-sender); }
.chat-bubble.system .sender { color: var(--system-sender); }

.chat-bubble .message {
    font-size: var(--type-section);
    line-height: var(--line-body);
    color: var(--text-primary);
    white-space: pre-wrap;
}

.chat-bubble[data-chat-markdown-enhanced] .message {
    white-space: normal;
}

/* Same opt-out idiom, applied to the rest of the class: a surface whose body is
   RENDERED markdown (block elements) must not also honour the bubble's
   `pre-wrap`, or every source newline becomes an extra blank line. The marker
   attribute is authored by the renderer that writes the markdown, so a surface
   that still shows plain multi-line text (an un-hydrated attempt detail) keeps
   pre-wrap and its authored line breaks. */
.chat-live-line-body,
[data-chat-markdown-enhanced].chat-live-line-title,
[data-chat-markdown-enhanced].skill-review-full,
[data-chat-markdown-enhanced].chat-review-attempt-detail {
    white-space: normal;
}

.chat-photo {
    max-width: 100%;
    border-radius: var(--radius-inner);
    cursor: pointer;
}

.chat-video {
    max-width: 100%;
    border-radius: var(--radius-inner);
}

.chat-bubble.user .message { color: var(--user-text); }
.chat-bubble.assistant .message { color: var(--assistant-text); }
.chat-bubble.system .message { color: var(--system-text); }

/* Shared block presentation: hosts opt in when mounting rich Markdown.
   Chat bubbles retain their own density; publisher HTML keeps its own tags. */
.ui-rich-content {
    overflow-wrap: anywhere;
    min-inline-size: 0;
}
.ui-rich-content ul,
.ui-rich-content ol {
    padding-inline-start: var(--space-5, 24px);
    margin-block: 6px;
}
.ui-rich-content li {
    margin-block: 3px;
}
/* Rendered markdown outside a chat bubble had no paragraph rhythm at all: the
   global reset zeroes <p> margins and nothing put them back, so a multi-
   paragraph card body read as one wall of text. */
.ui-rich-content p {
    margin-block: var(--space-2);
}
.ui-rich-content p:first-child { margin-block-start: 0; }
.ui-rich-content p:last-child { margin-block-end: 0; }

.chat-bubble.system.chat-skill-review {
    overflow: hidden;
}

.chat-bubble.system.chat-skill-review .sender,
.chat-bubble.system.chat-skill-review .msg-time {
    margin-left: 14px;
    margin-right: 14px;
}

.chat-bubble.system.chat-skill-review .sender {
    margin-top: 10px;
}

.chat-bubble.system.chat-skill-review .msg-time {
    margin-bottom: 10px;
}

.chat-bubble.system.chat-skill-review .message {
    white-space: normal;
}

.skill-review-disclosure {
    border-top: 1px solid rgba(var(--neutral-rgb), 0.05);
}

.skill-review-summary-button {
    width: 100%;
    border: 0;
    background: transparent;
    color: inherit;
    cursor: pointer;
    padding: 11px 14px;
    display: flex;
    flex-direction: column;
    gap: 7px;
    text-align: left;
}

.skill-review-summary-button:hover {
    background: var(--system-bubble-bg);
}

.skill-review-summary-main {
    color: var(--status-warn-fg);
    font-weight: 650;
}

.skill-review-summary-side {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    color: var(--text-meta);
    font-size: var(--type-meta);
}

.skill-review-meta {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.skill-review-toggle-label {
    flex-shrink: 0;
    color: var(--system-sender);
    font-weight: 650;
}

.skill-review-full {
    padding: 0 14px 14px;
    color: var(--system-text);
    white-space: pre-wrap;
    /* Full review bodies can run to hundreds of lines: scroll inside the
       card instead of re-bloating the chat column (owner-picked 60vh). */
    max-height: 60vh;
    overflow-y: auto;
}

/* The failure and its Retry (the shared small capsule) are one wrapping row, like the delivery sentence's actions. */
.skill-review-error {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    color: var(--status-error-fg);
}

/* A message waiting for its echo: faded on white; on dark it keeps full ink and its words below say so. */
.chat-bubble.pending {
    opacity: var(--message-pending-opacity);
    border-style: dashed;
}

/* The delivery sentence under an owner message. When it carries actions (Send again, Discard, Send as a new
   message) the sentence and its actions are ONE flex row: the same gap everywhere, wrapping on a phone, never a
   stack of full-width buttons (docs/DESIGN.md "Chat attachments"). */
.msg-pending {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-2);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-muted);
}
/* A doubt the owner may act on is a load-bearing caveat: meta ink, not the disabled step (docs/DESIGN.md section 3). */
.msg-pending:is([data-ingress-unconfirmed], [data-ingress-saved="unconfirmed"], [data-ingress-saved="undispatched"]) {
    color: var(--text-meta);
}
/* The plain saved fact is one quiet tick before the message's time, in the time line's ink (chat_attachments.js
   placeIngressNote): saved, never read or started, so there is no second tick. Its words stay its text, out of sight;
   a doubt or an action makes it a row of words again. */
.msg-time > .msg-pending:is([data-ingress-saved=""], [data-ingress-saved="pending"]) {
    display: inline-block;
    width: 11px;
    height: 11px;
    margin: 0 var(--space-1) 0 0;
    vertical-align: -1px;
    overflow: hidden;
    white-space: nowrap;
    text-indent: 100%;
    color: inherit;
    background: currentColor;
    forced-color-adjust: none;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.4 6.5 4.8 8.9 9.6 3.3' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.4 6.5 4.8 8.9 9.6 3.3' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}

.msg-routing-annotation {
    margin-top: 6px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The receipt that names a room carries the Project reference on its own line, not in a second padded row. */
.msg-routing-annotation.has-reference {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1) var(--space-2);
    margin-top: var(--space-2);
    overflow: visible;
    white-space: normal;
}

.msg-routing-annotation[data-annotation-status="pending"] {
    color: var(--text-disabled);
}

.msg-routing-annotation[data-annotation-status="needs_manual_target"],
.msg-routing-annotation[data-annotation-status="project_unavailable"] {
    color: var(--status-error-fg);
}

/* A file the host could not attach to the task: wraps (names must stay readable on a phone). */
.msg-attachment-outcome {
    margin-top: 6px;
    color: var(--status-warn-fg);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}

/* Air is spacing, never paleness: a timestamp keeps its meta ink. */
.msg-time {
    font-size: var(--type-meta);
    color: var(--text-secondary);
    opacity: 1;
    margin-top: 6px;
    text-align: right;
    transition: opacity 0.3s ease;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
    user-select: none;
}
/* A completion row's one compact time line shows the short form; the full instants stay its title and spoken text. */
.msg-time-full {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* A message the host accepted while its room was on Swarm: the quiet word before its time, the Subagents mark in the
   time line's own ink (DESIGN "Composer owner controls"). */
.msg-swarm { display: inline-flex; align-items: center; gap: var(--space-1); vertical-align: -2px; }
.msg-swarm > svg { width: 13px; height: 13px; flex: none; }
.msg-swarm :is(.chat-subagents-core, .chat-subagents-helper, .chat-subagents-link) { color: currentColor; }
.msg-swarm-sep { margin-inline: var(--space-1); }


.chat-bubble.user .msg-time {
    text-align: right;
}

.chat-bubble.assistant .msg-time {
    text-align: left;
}

.chat-bubble.system .msg-time {
    text-align: left;
}

/* Subdued progress messages. */
.chat-bubble.progress {
    border-left: 2px solid var(--accent-18);
    max-width: var(--feed-measure);
    opacity: 0.72;
}

.chat-bubble.progress .sender {
    font-size: var(--type-meta);
    color: var(--assistant-sender);
}

.chat-bubble.progress .message {
    font-size: var(--type-body);
    line-height: 1.45;
    color: var(--text-secondary);
}

.chat-live-card {
    align-self: flex-start;
    /* The left voice's one measure (#chat-messages --feed-measure): monotonic across the 620px column, so the card's
       internal narrow regime (<= 560px) starts only when the COLUMN itself is narrow. */
    width: var(--feed-measure);
    padding: 0;
    border-radius: var(--radius-object);
    border: 1px solid transparent;
    background: var(--voice-ouroboros-card);
    box-shadow: var(--elev-rest);
    overflow: hidden;
    container-type: inline-size;
    container-name: livecard;
    /* Conversion to the shared Project entry fades in calmly (P3). */
    transition: border-color 250ms ease, background-color 250ms ease, box-shadow 250ms ease;
}

.chat-live-card[hidden] {
    display: none;
}

/* A long press on touch (chat_row_menu.js; owner 19.10, 4A): the row sinks a little while the finger holds, lifts over
   the dimmed feed while its menu is open, and settles back. The dimming is the row's own shadow spread over the feed,
   so only the feed dims while the row and its menu stay bright; it fades in and out and never grows. A row swiped to
   reply (chat_row_swipe.js) follows the finger and settles back on the same curve. */
:is(.chat-bubble, .chat-live-card)[data-row-press] {
    position: relative;
    z-index: 3;
    box-shadow: var(--elev-rest), 0 0 0 100vmax transparent;
    transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.24s ease-out;
    /* The row's menu answers the press: iOS would also select a word under it and open its own edit menu over ours. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}
:is(.chat-bubble, .chat-live-card)[data-row-press="hold"] { transform: scale(0.985); }
:is(.chat-bubble, .chat-live-card)[data-row-press="lift"] {
    transform: scale(1.012);
    box-shadow: var(--elev-float), 0 0 0 100vmax var(--scrim);
    transition: transform 0.24s var(--ease-sheet), box-shadow 0.24s ease-out;
}
:is(.chat-bubble, .chat-live-card)[data-row-swipe] {
    position: relative;
    transform: translateX(calc(-1 * var(--row-swipe, 0px)));
}
/* The swipe's own motion wins over a closed menu's lift still on its way back: 1:1 under the finger, its curve home. */
:is(.chat-bubble, .chat-live-card)[data-row-swipe="drag"] { transition: none; }
:is(.chat-bubble, .chat-live-card)[data-row-swipe="settle"] { transition: transform 0.22s var(--ease-sheet); }
/* The reply mark at the room's right side: it fills as the row nears the reply point and takes the accent there. */
.chat-row-reply-mark {
    position: fixed;
    z-index: var(--z-row-reply-mark);
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--surface-capsule);
    color: var(--text-secondary);
    box-shadow: var(--elev-rest);
    opacity: var(--reply-k, 0);
    transform: scale(calc(0.6 + 0.4 * var(--reply-k, 0)));
    pointer-events: none;
}
.chat-row-reply-mark[data-armed="true"] { color: var(--accent); }
.chat-row-reply-mark[data-leaving="true"] { opacity: 0; transition: opacity 0.16s ease-out; }
@media (prefers-reduced-motion: reduce) {
    :is(.chat-bubble, .chat-live-card)[data-row-press]:not([data-row-swipe]) { transform: none; transition: box-shadow 0.24s ease-out; }
    :is(.chat-bubble, .chat-live-card)[data-row-swipe="settle"] { transition: none; }
}

/* The portal (work_portal.js): the same card in Main, collapsed for good, while its work lives in a
   Project room. Chip + quiet fact, title, one narration line (two at most), the foot (the meta line,
   then one action row led by the Project reference). The room holds the timeline, reviews and children,
   so they never render here, and the card has no disclosure. */
.chat-live-card[data-portal="1"] > .chat-live-timeline,
.chat-live-card[data-portal="1"] > [data-live-reviews-host],
.chat-live-card[data-portal="1"] > .chat-subagents,
.chat-live-card[data-portal="1"] > .chat-live-summary-button .chat-live-summary-side,
.chat-live-card[data-portal="1"] > .chat-live-summary-button .chat-live-review-summary,
.chat-live-card[data-portal="1"] > .chat-live-summary-button .chat-live-doors {
    display: none;
}

.chat-live-card[data-portal="1"]:not([data-expanded="1"]) > .chat-live-summary-button .chat-live-activity {
    -webkit-line-clamp: 2;
    line-clamp: 2;
}
/* A work portal is compact (owner 26.10, B): its meta line follows the summary at the summary's own gap. */
.chat-live-card[data-portal="1"] > .chat-live-summary-button { padding-bottom: var(--space-2); }
.chat-live-card[data-portal="1"] > .chat-live-foot { padding-top: 0; }

/* A binding whose Main receipt is not durable keeps its visible gap mark. */
.chat-live-card[data-receipt] {
    border-style: dashed;
}

/* The summary band (chip, title, episodes, latest line) is reading content: selectable, never a control,
   and the pointer paints nothing on it or on the body below (owner 17.10: the half-card tint read as a seam). */
.chat-live-summary-button {
    box-sizing: border-box;
    width: 100%;
    /* The card's inset: 20px at the sides, 16px on top; the foot below closes it. */
    padding: 16px 20px 12px;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.chat-live-summary {
    display: flex;
    align-items: flex-start;
    /* A flex item shrinks below its text only with min-width: 0; the nested
       one-line title relies on that to ellipsize instead of pushing the side
       controls past the card edge. */
    min-width: 0;
    justify-content: space-between;
    gap: var(--space-3);
}

/* A root card reads top to bottom (owner 30.10, B): its status row (chip, quiet fact and clock) with its doors on the
   right, then the work's name on a line of its own, at the card-title size and title weight 500. */
.chat-live-card:not(.subagent) > .chat-live-summary-button > .chat-live-summary {
    flex-wrap: wrap;
    align-items: center;
    row-gap: var(--space-2);
}

/* The status box spans its row and centres its own chip, so an empty status (a terminal projection) starts at
   the row's top with the doors instead of floating to the row's middle. */
.chat-live-card:not(.subagent) > .chat-live-summary-button .chat-live-summary-main {
    align-self: stretch;
}

.chat-live-card:not(.subagent) > .chat-live-summary-button .chat-live-title {
    order: 3;
    flex: 1 1 100%;
    font-size: var(--type-section);
    line-height: var(--line-title);
}

/* The whole name wraps at the reading measure of the line under it (a portal keeps its own two lines). */
.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-summary-button .chat-live-title {
    line-height: 1.45;
}

/* A status row of the doors' height, whose box takes the room the doors leave: its clock wraps under the chip before
   a door leaves the row. A portal has no doors and keeps its chip's own box. */
.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-summary-button .chat-live-summary-main {
    flex: 1 1 0;
    min-width: min-content;
    min-height: var(--space-5);
}

/* The card's doors: `Reviews N` (once there are reviews) then `Details`, one quiet group at the end of the status
   row and on its first line (a narrow card's clock wraps under the chip, never a door), each paying its padding back
   at the group's end so the words stand where words stand. A child keeps `Details` alone in its identity row (its
   Reviews door docks in its foot): the group adds no box there. */
.chat-live-doors {
    display: contents;
}

.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-summary-button .chat-live-doors {
    order: 2;
    display: inline-flex;
    align-self: flex-start;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--space-5);
    margin-inline: auto calc(var(--space-2) * -1);
}

.chat-live-card:not(.subagent) > .chat-live-summary-button .chat-live-doors > .chat-live-summary-side {
    margin-inline: 0;
}

/* Beside Details, the Reviews door is its twin: the same box, words and chevron. */
.chat-live-card:not(.subagent) > .chat-live-summary-button .chat-live-doors > .chat-live-review-summary {
    box-sizing: content-box;
    min-height: calc(1.45 * var(--type-body));
    margin: calc(var(--space-1) * -1) 0;
    padding: var(--space-1) var(--space-2);
    color: var(--text-secondary);
    font-weight: inherit;
}

.chat-live-card:not(.subagent) > .chat-live-summary-button .chat-live-doors > .chat-live-review-summary > .chat-live-chevron {
    color: var(--text-disabled);
}

/* The card's clock beside its chip (chat_activity.js paintHeadClock): `updated` or `started` in the meta line's ink.
   A child's clock ends its meta line; a shell that became a child never keeps a root's clock in its identity row. */
.chat-live-when {
    color: var(--text-secondary);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    white-space: nowrap;
}

.chat-live-card.subagent > .chat-live-summary-button .chat-live-when {
    display: none;
}

/* Chip + typing dots only; the title is their sibling so a narrow container can
   reorder it under the side controls. */
.chat-live-summary-main {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-2);
    flex: 0 1 auto;
    max-width: 100%;
    min-width: 0;
}

/* The card's one disclosure: "N notes · Show details ⌄" is a real button, the only thing on the card that
   opens it. A quiet control: no body at rest, the quiet tint under a hovering pointer, a press deepens it, the
   shared ring for the keyboard. Its pill's padding is paid back by an equal negative margin, so it stands where
   its words stand. */
.chat-live-summary-side {
    position: relative;
    box-sizing: content-box;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
    /* One title line (title type size, not the inherited body size). */
    min-height: calc(1.45 * var(--type-body));
    margin: calc(var(--space-1) * -1) calc(var(--space-2) * -1);
    padding: var(--space-1) var(--space-2);
    border: 0;
    border-radius: var(--pill-radius);
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
    transition: var(--motion-tone);
}

@media (hover: hover) {
    .chat-live-summary-side:hover { background: var(--tone-quiet-hover); }
}

.chat-live-summary-side:active {
    background: var(--tone-quiet-hover);
    filter: var(--press);
}

.chat-live-summary-side:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

.chat-live-phase {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    border-radius: 999px;
    border: 1px solid transparent;
    background: var(--status-neutral-bg);
    color: var(--text-secondary);
    font-size: var(--type-meta);
    font-weight: 600;
    text-transform: none;
}

/* The task status chip (DESIGN "Status marks"): a flat tint with the shared status mark first and the chip's words,
   in its explicit pair. Its body tone is the view's (task_phase_chip.js writes data-chip-tone): the live word crimson,
   the owner's move amber, a known outcome in its tone (Done with warnings succeeds, Stopped is neutral), anything else
   the neutral chip above. The mark takes the chip's ink; only its moving dots loop. */
.chat-live-phase {
    gap: calc(var(--space-1) * 1.5);
    padding: calc(var(--space-1) / 2) var(--space-3) calc(var(--space-1) / 2) var(--space-2);
    line-height: var(--line-meta);
    font-weight: 500;
    white-space: nowrap;
}

/* A live chip keeps the moving dots' width when the owner's dot takes their place, so its word never moves. */
.chat-live-phase[data-chip-tone="live"] > .status-mark { min-width: calc(3 * var(--mark-dot) + 2 * var(--mark-gap)); }

.chat-live-phase[data-chip-tone="live"] {
    color: var(--status-live-fg);
    background: var(--status-live-bg);
}

.chat-live-phase[data-chip-tone="owner"] {
    color: var(--status-warn-fg);
    background: var(--status-warn-bg);
}

.chat-live-phase[data-chip-tone="ok"] {
    color: var(--status-ok-fg);
    background: var(--status-ok-bg);
}

.chat-live-phase[data-chip-tone="failed"] {
    color: var(--status-error-fg);
    background: var(--status-error-bg);
}

/* Logs phase pills name the status triples by class (DESIGN "The tone primitive"); the
   saturated --green/--amber/--red are dot-and-indicator hues, not small-text hues. A Logs task headline that settled
   is its task's outcome and takes the cards' tone (logs.js writes data-chip-tone): Done with warnings green, Stopped
   neutral. A warn-level log event stays amber: a warning worth reading. */
.log-entry .log-phase:is(.done, .ready, .ok, .result, .usage, .metrics),
.log-entry .log-phase[data-chip-tone="ok"] {
    color: var(--status-ok-fg);
    border-color: var(--status-ok-border);
    background: var(--status-ok-bg);
}

.log-entry .log-phase:is(.warn, .empty):not([data-chip-tone]) {
    color: var(--status-warn-fg);
    border-color: var(--status-warn-border);
    background: var(--status-warn-bg);
}

.log-entry .log-phase:is(.error, .timeout, .lifecycle_error) {
    color: var(--status-error-fg);
    border-color: var(--status-error-border);
    background: var(--status-error-bg);
}

.chat-live-title {
    font-size: var(--type-body);
    font-weight: 500;
    color: var(--text-primary);
    flex: 1;
    min-width: 0;
    line-height: 1.45;
    white-space: pre-wrap;
    /* A derived name is the request's own first line, so it can be one long
       unbroken token (a URL or a path). Without a wrap escape it overflows the
       card on every narrow surface, not just the phone container query. */
    overflow-wrap: anywhere;
}

/* A collapsed child and a Main portal keep a compact geometry; a root card's name and latest line wrap whole
   (owner 30.10, B). Expanded cards keep the ordinary wrapping behavior; complete activity/review/result text lives
   in their existing timeline disclosure rather than in a mouse-only tooltip. `:where` keeps the old weight, so the
   `:empty` rules below still win. */
.chat-live-card:where(.subagent, [data-portal="1"]):not([data-expanded="1"]) > .chat-live-summary-button .chat-live-title,
.chat-live-card:where(.subagent, [data-portal="1"]):not([data-expanded="1"]) > .chat-live-summary-button .chat-live-activity {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
}

/* One reserved title line (a child's or a portal's coined two-line name still clamps to two; the
   stable-viewport seam absorbs that growth, as it does a root's longer name). */
.chat-live-card:not([data-expanded="1"]) > .chat-live-summary-button .chat-live-title {
    min-height: 1.45em;
}

/* A block without work carries no title placeholder: the collapsed geometry
   reserves nothing for an empty title (review round 1: the clamp and the
   reserved line above outranked the general empty-title rule). */
.chat-live-card:not([data-expanded="1"]) > .chat-live-summary-button .chat-live-title:empty {
    display: none;
    min-height: 0;
}

/* Activity grows with useful content. Details retain the complete source. */
.chat-live-activity {
    color: var(--text-meta);
    font-size: var(--type-body);
    line-height: var(--line-body);
    flex: 1 1 100%;
    min-width: 0;
    overflow-wrap: anywhere;
}
/* A root card's latest line follows its name closely: the two are one statement (B's 4px). */
.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-summary-button {
    row-gap: var(--space-1);
}
.chat-live-card > .chat-live-summary-button .chat-live-activity:empty { display: none; }

/* The notes count stays the disclosure's name (`36 notes · 1 child Details`) but leaves its row: the
   collapsed header shows the one word and the chevron (owner, refined A 21.10). */
.chat-live-count {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.chat-live-toggle {
    color: var(--text-secondary);
    font-size: var(--type-meta);
    white-space: nowrap;
}

.chat-live-chevron {
    color: var(--text-disabled);
    transition: transform 0.18s ease;
}

/* Details' own state, never the card's: the Reviews door beside it turns only with its own (below). */
.chat-live-summary-side[aria-expanded="true"] > .chat-live-chevron {
    transform: rotate(180deg);
}

.chat-live-meta {
    display: flex;
    flex-wrap: wrap;
    /* One space's width after a separator (the space before it is real text inside the part). */
    gap: 0 var(--space-1);
    flex: 1 1 auto;
    min-width: 0;
    color: var(--text-disabled);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

.chat-live-card:not([data-expanded="1"]) > .chat-live-foot > .chat-live-meta {
    min-height: 1.35em;
}

/* The card's foot: the meta line (who ran it · how long · tool calls · cost), then ONE action row of the commands
   over the work, right-aligned: [Turn into project] [Resume] [Stop…]; Reply is the row menu's (DESIGN "Replies").
   A root card's foot is one band (owner 30.10, B, below): the meta at its left, the commands at its right end. A
   child's row leads with its docked Reviews count and a work portal's with its Project reference, each under the
   meta line (with its clock) at every width. */
/* The work card's foot (meta line, its one action row) shares the card's 20px insets. */
.chat-live-foot {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    padding: var(--space-2) 20px var(--space-3);
}

.chat-live-foot-end {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2) var(--space-3);
    min-width: 0;
}

.chat-live-foot-end > .chat-live-review-summary {
    margin-inline-end: auto;
}

/* A root card's action row, wherever it stands, keeps the controls' height, so a row of words breathes like a row
   of buttons. */
.chat-live-card:not(.subagent) > .chat-live-foot > .chat-live-foot-end {
    min-height: calc(var(--space-5) + var(--space-1));
}

/* The root card's band: facts left, the command cluster right on the controls' height; when the two do not fit the
   cluster wraps whole under the facts, so no fact is ever stranded under a button. It sits 12px under the card's
   line and keeps the card's 16px inset below. With no receipt, fact or command it closes the card on its summary
   (`> *`, never `:empty`, inside `:has()`: WebKit re-matches that when a late Continue lands). */
.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-foot {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    /* Wrapped, the commands stand a row's air under the facts (U121). */
    gap: var(--space-3) var(--space-4);
    min-height: calc(var(--space-5) + var(--space-1));
    padding-top: 0;
    padding-bottom: var(--space-4);
}

.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-foot > .chat-live-meta {
    flex: 0 1 auto;
}

.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-foot > .chat-live-foot-end {
    margin-inline-start: auto;
}

/* A Swarm root's receipt stays one line of its own, first in the foot (DESIGN "Composer owner controls"), the band under
   it; with no fact beside it, the empty meta adds no line. */
.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-foot > .chat-live-swarm-receipt {
    flex: 0 0 100%;
}

.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-foot > .chat-live-swarm-receipt ~ .chat-live-meta:empty {
    display: none;
}

.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-foot:not(:has(> .chat-live-swarm-receipt, > .chat-live-meta > *, > .chat-live-foot-end > .chat-live-actions > *)) {
    display: none;
}

.chat-live-card:not(.subagent):not([data-portal="1"]) > .chat-live-summary-button:not(:has(~ .chat-live-foot > .chat-live-swarm-receipt, ~ .chat-live-foot > .chat-live-meta > *, ~ .chat-live-foot > .chat-live-foot-end > .chat-live-actions > *)) {
    padding-bottom: var(--space-4);
}

/* Under a finger the foot's commands (and a child's docked Reviews door) keep their 28px look and gain the shared hit
   area (ui.css .ui-hit-area), which reaches out above and below; rows that wrap on a phone part by that overshoot, so a
   finger's targets never overlap. A root's doors stand in its head, a row away from them. */
@media (pointer: coarse) {
    .chat-live-foot-end,
    .chat-live-foot-end > .chat-live-actions {
        row-gap: calc(var(--touch-target-min) - var(--space-5) - var(--space-1));
    }
}

/* Shown once it holds something. Written without :empty inside :has(): WebKit does not re-match that when a control
   is added to the actions row later (a Continue offered after a reload stayed unpainted in Safari). */
.chat-live-foot-end:not(:has(> .chat-live-review-summary:not([hidden]), > .chat-live-actions > *, > .chat-live-bound-pointer)) {
    display: none;
}

/* A row led by the Project reference stays one line at every width: its controls keep their size and the capsule's
   name gives way first. */
.chat-live-foot-end:has(> .chat-live-bound-pointer) { flex-wrap: nowrap; }
.chat-live-foot-end > .chat-live-bound-pointer ~ .chat-live-actions { flex-shrink: 0; }

/* Episodes of one work (Continue): one meta line each under the title, never a chip column. */
.chat-live-episodes {
    display: flex;
    flex-direction: column;
    gap: calc(var(--space-1) / 2);
    min-width: 0;
}

.chat-live-episodes[hidden] {
    display: none;
}

.chat-live-episode {
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}

.chat-live-episode-label {
    font-weight: 500;
}

.chat-live-episode-outcome {
    font-weight: 500;
}

.chat-live-episode-outcome[data-tone="ok"] { color: var(--status-ok-fg); }
.chat-live-episode-outcome[data-tone="failed"] { color: var(--status-error-fg); }
.chat-live-episode-outcome[data-tone="neutral"] { color: var(--status-neutral-fg); }

/* Earlier physical episodes disclose their existing content, never another work/control card. */
.chat-live-episode-history { margin: var(--space-2) var(--space-4) var(--space-3); }
.chat-live-episode-history[hidden],
.chat-live-card:not([data-expanded="1"]) > .chat-live-episode-history { display: none; }
.chat-episode-history { border-top: 1px solid var(--divider); min-width: 0; }
.chat-episode-history > summary {
    cursor: pointer;
    padding-block: var(--space-2);
    color: var(--text-meta);
    font-size: var(--type-meta);
    overflow-wrap: anywhere;
}
.chat-episode-history > summary:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
.chat-episode-history[open] > [data-live-reviews-host] > .chat-live-reviews { display: block; }

/* Why a chip word or an amount reads as it does: part of the expanded card, which its
   Show details button opens by pointer, touch and keyboard (never a hover-only title); selectable. */
.chat-live-details {
    display: flex;
    flex-direction: column;
    min-width: 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}
.chat-live-details > div { padding: 0 14px var(--space-1); }
.chat-live-card:not([data-expanded="1"]) > .chat-live-details { display: none; }

/* The card's facts beyond its compact line (coordinator, model execution, origin, counts): a labelled
   list at the meta size, label in secondary ink and value in primary, selectable like the rest. */
.chat-live-facts {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: var(--space-1) var(--space-4);
    margin: 0;
    padding: 0 14px var(--space-2);
}
.chat-live-facts[hidden] { display: none; }
.chat-live-fact { display: contents; }
.chat-live-fact > dt { color: var(--text-secondary); }
.chat-live-fact > dd { margin: 0; min-width: 0; color: var(--text-primary); overflow-wrap: anywhere; }
@container livecard (max-width: 400px) {
    .chat-live-facts { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
    .chat-live-fact > dd { margin-bottom: var(--space-1); }
}

/* The secondary status fact beside the outcome chip (a running finalization).
   Neutral by rule: the outcome owns the status colour, this only says the task
   has not let go yet, so it stays a quiet sentence rather than a second chip, and never moves. */
.chat-live-phase-secondary {
    min-width: 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

.chat-live-phase-secondary[hidden] {
    display: none;
}

/* HQ1: an applied owner hurry, never a chat bubble — the house fast-forward vector
   beside the chip, named by its aria-label (task_phase_chip.markOwnerHurry). */
.chat-live-hurry {
    display: inline-flex;
    align-items: center;
    color: var(--text-meta);
}

.chat-live-meta-text {
    display: inline-flex;
    align-items: center;
    color: var(--text-secondary);
    font-size: var(--type-meta);
}

.chat-live-meta-part {
    min-width: 0;
}

/* The ` · ` separator rides inside the fact before it (chat_activity.metaLineHtml). A plain-text fact is an
   inline block: it moves to the next line whole, and when it is longer than a line its separator follows its last
   word in the text flow (an inline-flex box would park the dot at the far right of the wrapped fact). */
.chat-live-meta-part > .chat-live-meta-text:not(.chat-live-effort) {
    display: inline-block;
}

/* A Swarm root's one receipt line, first in its foot, in the meta line's type (DESIGN "Composer owner controls"). */
.chat-live-swarm-receipt {
    color: var(--text-secondary);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}


/* Reviews A (owner 25.10, quiz answer A; DESIGN §5 "Reviews inside task cards"). The door is a quiet control
   like the card's Details, its chevron turning when open; a closed check is one row with no fill and only the
   opened check takes the one tray; a reviewer is two lines (who and their outcome, then one line of what they said
   or why not) with Read into the document reader; one Record facts disclosure keeps the record. A state is the
   shared status sentence (`.ui-status[data-tone]`) or, for live work, the moving status mark. Narrow cards follow
   the card's own width (`livecard`), never the viewport. */
.chat-live-review-summary {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    flex: 0 0 auto;
    min-height: calc(var(--space-5) + var(--space-1));
    margin: 0 calc(var(--space-2) * -1);
    padding: 0 var(--space-2);
    border: 0;
    border-radius: var(--pill-radius);
    background: transparent;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    transition: var(--motion-tone);
}

/* The card's Details chevron after the door's words (its one text node, review_presentation.js), as an element: the
   door's ::after is the touch hit area (.ui-hit-area), so a finger meets a 44px door with the chevron beside its words. */
.chat-live-review-summary > .chat-live-chevron { flex: none; color: var(--text-secondary); }
.chat-live-review-summary[aria-expanded="true"] > .chat-live-chevron { transform: rotate(180deg); }

@media (hover: hover) {
    .chat-live-review-summary:hover { background: var(--tone-quiet-hover); }
}

.chat-live-review-summary:active {
    background: var(--tone-quiet-hover);
    filter: var(--press);
}

.chat-live-review-summary:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

[data-live-reviews-host] > .chat-live-reviews {
    display: block;
    margin: 0 var(--space-2) var(--space-3);
}

[data-live-reviews-host] > .chat-live-reviews[data-expanded="0"]:not([data-review-hydrate-visible]) {
    display: none;
}

.review-checks {
    display: flex;
    flex-direction: column;
    gap: calc(var(--space-1) / 2);
}

.review-check {
    min-width: 0;
    border-radius: var(--radius-inner);
}

.review-check[data-open="1"] {
    margin: var(--space-1) 0 var(--space-2);
    background: var(--surface-tray);
}

.review-check-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: baseline;
    gap: calc(var(--space-1) / 2) var(--space-3);
    width: 100%;
    padding: var(--space-2);
    border: 0;
    border-radius: var(--radius-inner);
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: start;
    cursor: pointer;
    transition: var(--motion-tone);
}

@media (hover: hover) {
    .review-check:not([data-open="1"]) > .review-check-head:hover { background: var(--tone-quiet-hover); }
}

.review-check-head:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

.review-check-subject {
    display: -webkit-box;
    min-width: 0;
    overflow: hidden;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    color: var(--text-primary);
    font-size: var(--type-body);
    line-height: var(--line-title);
    font-weight: 600;
    overflow-wrap: anywhere;
}

.review-check-meta {
    grid-column: 1 / -1;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}

.review-check-head > .review-chevron {
    align-self: center;
    color: var(--text-secondary);
    transition: transform 0.18s ease;
}

.review-check[data-open="1"] > .review-check-head > .review-chevron { transform: rotate(180deg); }

.review-check-body { padding: 0 var(--space-2) var(--space-3); }

/* A status word's weight (DESIGN bible "Type"); live work keeps the moving dots' width beside its word. */
.review-state {
    flex: none;
    white-space: nowrap;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    font-weight: 500;
}

.review-state[data-tone="working"] {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-meta);
}

.review-seats,
.review-attempt {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-width: 0;
}

.review-seat { min-width: 0; }

.review-seat-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: calc(var(--space-1) / 2) var(--space-2);
    min-width: 0;
}

.review-seat-head > .review-state { margin-inline-start: auto; }

.review-seat-head > .harness-identity {
    align-self: center;
    color: var(--text-secondary);
    font-size: var(--type-meta);
}

.review-seat-name {
    min-width: 0;
    color: var(--text-primary);
    font-size: var(--type-body);
    line-height: var(--line-title);
    font-weight: 500;
    overflow-wrap: anywhere;
}

.review-seat-note,
.review-seat-role,
.review-seat-meta,
.review-ask-line,
.review-check-line,
.review-history,
.review-hydrate,
.chat-review-execution-model {
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}

.review-ask-line { margin: 0; }

.review-seat-line {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    min-width: 0;
    margin-top: calc(var(--space-1) / 2);
}

.review-seat-meta { flex: none; white-space: nowrap; }

.review-line {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    overflow: hidden;
    color: var(--text-primary);
    font-size: var(--type-body);
    line-height: var(--line-body);
    white-space: nowrap;
    text-overflow: ellipsis;
}

.review-line-wrap {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    white-space: normal;
    overflow-wrap: anywhere;
}

/* A record's relation to the code that landed is read whole at every width (a narrow card's clamp included). */
.review-line.review-line-whole {
    display: block;
    overflow: visible;
    -webkit-line-clamp: none;
    line-clamp: none;
    white-space: normal;
    text-overflow: clip;
    overflow-wrap: anywhere;
}

.review-line-quiet,
.review-line-by { color: var(--text-meta); }

.review-sev {
    margin-inline-end: var(--space-1);
    color: var(--text-meta);
    font-size: var(--type-meta);
    font-weight: 600;
}

.review-sev[data-tone="error"] { color: var(--status-error-fg); }

.review-warning { margin: 0 0 var(--space-3); }

/* Read: a quiet link-button (words and a chevron), the touch area extended by .ui-hit-area. */
.review-read {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: calc(var(--space-1) / 2);
    min-height: var(--space-5);
    margin: 0 calc(var(--space-1) * -1);
    padding: 0 var(--space-1);
    border: 0;
    border-radius: var(--pill-radius);
    background: transparent;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    font-weight: 600;
    cursor: pointer;
    transition: var(--motion-tone);
}

@media (hover: hover) {
    .review-read:hover:not(:disabled) { background: var(--tone-quiet-hover); }
}

.review-read:active { filter: var(--press); }
.review-read:disabled { color: var(--text-meta); cursor: default; }

.review-read:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

.review-check-foot,
.review-history,
.review-hydrate {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-1) var(--space-4);
}

.review-check-foot {
    margin-top: var(--space-3);
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

.review-check-line { flex: 1 1 auto; min-width: 0; }
.review-history { margin: var(--space-2) var(--space-2) 0; }
.review-hydrate { margin: 0 var(--space-2) var(--space-2); }

/* The second layer: the record as a labelled list at the meta size, behind one quiet disclosure; a long record
   scrolls inside its own box, whose reading position an update keeps. */
.review-facts { min-width: 0; }
.review-facts[open] { flex-basis: 100%; }

.review-facts-summary {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-height: var(--space-5);
    border-radius: var(--pill-radius);
    color: var(--text-secondary);
    font-weight: 600;
    list-style: none;
    cursor: pointer;
}

.review-facts-summary::-webkit-details-marker { display: none; }
.review-facts-summary > svg { transition: transform 0.18s ease; }
.review-facts[open] > .review-facts-summary > svg { transform: rotate(90deg); }

.review-facts-summary:focus-visible,
.review-facts-body:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

.review-facts-body {
    max-height: 40vh;
    margin-top: var(--space-2);
    overflow-y: auto;
    overscroll-behavior: contain;
    border-radius: var(--radius-inner);
}

.review-facts-list {
    display: grid;
    /* A label takes at most 40% and wraps: a long one (`Review panel <record id>`) never squeezes the values. */
    grid-template-columns: fit-content(40%) minmax(0, 1fr);
    gap: var(--space-1) var(--space-4);
    margin: 0;
}

.review-facts-list dt { min-width: 0; color: var(--text-secondary); overflow-wrap: anywhere; }

.review-facts-list dd {
    min-width: 0;
    margin: 0;
    color: var(--text-primary);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.review-facts-list dd.review-facts-note { grid-column: 1 / -1; }

/* A narrow card (a phone, a narrow Project panel) keeps every word: a check's state moves under its subject and a
   reviewer's line wraps to two lines before Read, as approved A draws it at 390. */
@container livecard (max-width: 560px) {
    .review-check-head { grid-template-columns: minmax(0, 1fr) auto; }
    .review-check-head > .review-state { grid-column: 1; grid-row: 2; justify-self: start; }
    .review-check-head > .review-chevron { grid-column: 2; grid-row: 1; }
    .review-check-meta { grid-row: 3; }
    .review-seat-head > .review-state { margin-inline-start: 0; }
    .review-seat-line { flex-wrap: wrap; }

    .review-line {
        display: -webkit-box;
        flex-basis: 100%;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        line-clamp: 2;
        white-space: normal;
        overflow-wrap: anywhere;
    }
}

@media (prefers-reduced-motion: reduce) {
    .chat-live-review-summary > .chat-live-chevron,
    .review-check-head > .review-chevron,
    .review-facts-summary > svg { transition: none; }
}

.chat-live-timeline {
    padding: var(--space-3) var(--space-4);
    display: none;
    flex-direction: column;
    gap: var(--space-2);
}

.chat-live-card[data-expanded="1"] > .chat-live-timeline,
.chat-episode-history[open] > .chat-live-timeline {
    display: flex;
    border-top: 1px solid rgba(var(--neutral-rgb), 0.05);
    max-height: 420px;
    overflow-y: auto;
}

/* The facts and notes Details opens lie under the card's own lines, before its children; earlier episodes follow the
   children (chat.js ensureSubagentContainer). While the card holds children its facts and notes each stay short and
   scroll inside, so the first child stays in reach right below them: opening Details never sends the children after
   the whole timeline, and none of them folds. */
.chat-live-card[data-children][data-expanded="1"] > .chat-live-details {
    max-height: min(7.5rem, calc(var(--vvh) * 0.15));
    overflow-y: auto;
}
.chat-live-card[data-children][data-expanded="1"] > .chat-live-timeline {
    max-height: min(13rem, calc(var(--vvh) * 0.26));
}
/* While one scrolls it is a named region the keyboard focuses and scrolls (chat.js, scroll_fade.js): the one ring,
   drawn inside its box, which the card clips. */
:is(.chat-live-details, .chat-live-timeline)[tabindex]:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}

.chat-subagents {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-block: 0 var(--space-3);
    margin-inline: var(--space-5) var(--space-4);
    padding-inline-start: var(--space-3);
    border-inline-start: 1px solid var(--accent-18);
}

.chat-subagents:empty {
    display: none;
}

/* The one narrow-column rule: any chat column of 620px or less (a phone, a
   narrow Project panel) gives bubbles and root cards its full width, without the
   wide-screen gutters. It is owned by the COLUMN, not the viewport, and stays
   late enough to beat the base 80% rules above. */
@container chatcol (max-width: 620px) {
    .chat-bubble {
        max-width: 100%;
    }

    .chat-bubble.user {
        margin-inline-start: 0;
    }

    /* The question card takes the narrow column's full width like every other bubble, so a column of
       bubbles shares one right edge (DESIGN §5). Two classes: the base 560px rule comes later in the file. */
    .chat-bubble > .chat-quiz-card {
        max-width: none;
    }
}

/* Internal card geometry is owned by the CARD'S actual width. This catches a
   deeply nested child even when the outer chat column is wide. The main row
   takes a real 100% flex basis; flex-wrap alone leaves flex:1 at a zero basis
   and recreates the one-letter-wide title. This component threshold stays
   below the separate chat-column gutter breakpoint so the launcher-default
   root and first child keep regular geometry. Nested indentation is paid once,
   then flattened so deeper cards cannot consume the text column. */
@container livecard (max-width: 560px) {
    .chat-live-summary {
        flex-wrap: wrap;
    }

    .chat-live-summary-main {
        flex: 0 0 auto;
    }

    .chat-live-title {
        order: 3;
        flex: 1 1 100%;
        overflow-wrap: anywhere;
    }

    .chat-live-summary-side {
        order: 2;
        margin-inline-start: auto;
        max-width: 100%;
        flex-wrap: wrap;
        justify-content: flex-end;
    }

    .chat-live-line-toggle {
        flex-wrap: wrap;
    }

    .chat-live-line-head {
        flex: 1 1 100%;
    }

    .chat-live-line-title {
        overflow-wrap: anywhere;
    }

    .chat-live-line-expand-label {
        margin-inline-start: auto;
    }

    /* Equal inline inset on both sides, paid once: a child sits --space-1 inside the thread line on the left
       and inside its root card's border on the right, so no child border meets the root's border. Deeper
       levels keep only their rail and frame; any wider per-level inset collapses a depth-10 child on a phone. */
    .chat-subagents {
        margin-inline: 0;
        padding-inline: var(--space-1);
    }

    .chat-subagents .chat-subagents {
        padding-inline: 0;
    }

    /* A COLLAPSED nested child keeps its identity row — status chip ·
       role · Details · chevron — over its model/executor metadata (DESIGN.md
       §5) instead of dropping the title to a third band once its own inline
       width crosses this threshold (#623). The side controls keep their size.
       The role is sized by its own words, not by what the status leaves: when
       the row is short both yield alike, a long status wrapping inside its chip
       (never narrower than its longest word) and the role taking a second line,
       before either is cut. Status and role start from nothing and grow to their
       own width, so the row wraps only when even those floors leave no room for
       the controls (a 320px phone): then the controls alone move under the role,
       never past the card's edge. Higher specificity than the .chat-live-title
       rules around it, so the cascade decides, not source order. */
    .chat-live-card.subagent:not([data-expanded="1"]) > .chat-live-summary-button > .chat-live-summary {
        flex-wrap: wrap;
    }

    .chat-live-card.subagent:not([data-expanded="1"]) > .chat-live-summary-button .chat-live-summary-main {
        flex: 1 1 0;
        min-width: min-content;
        max-width: max-content;
    }

    .chat-live-card.subagent:not([data-expanded="1"]) > .chat-live-summary-button .chat-live-phase {
        min-width: min-content;
        white-space: normal;
    }

    .chat-live-card.subagent:not([data-expanded="1"]) > .chat-live-summary-button > .chat-live-summary > .chat-live-title {
        order: 0;
        flex: 1 1 0;
        min-width: 4em;
        max-width: max-content;
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        line-clamp: 2;
        white-space: normal;
    }

    .chat-live-card.subagent:not([data-expanded="1"]) > .chat-live-summary-button .chat-live-summary-side {
        flex: none;
        flex-wrap: nowrap;
        margin-inline-start: auto;
    }
}

.chat-live-card.subagent {
    width: 100%;
    max-width: 100%;
}

/* An opaque secondary surface prevents translucent tints accumulating at depth.
   Nested frames and parent-child rails remain the hierarchy cue. */
.chat-live-card.subagent > .chat-live-foot {
    padding-block: var(--space-1) var(--space-2);
}

/* A child with nothing for its foot (no meta, no count, no control) closes on its summary. */
.chat-live-card.subagent > .chat-live-foot:not(:has(> .chat-live-meta:not(:empty), > .chat-live-foot-end > .chat-live-review-summary:not([hidden]), > .chat-live-foot-end > .chat-live-actions:not(:empty))) {
    display: none;
}

.chat-live-card.subagent > .chat-live-summary-button:not(:has(~ .chat-live-foot > .chat-live-meta:not(:empty), ~ .chat-live-foot > .chat-live-foot-end > .chat-live-review-summary:not([hidden]), ~ .chat-live-foot > .chat-live-foot-end > .chat-live-actions:not(:empty))) {
    padding-bottom: var(--space-2);
}

/* A nested child is subordinate to its root: quieter ink, and collapsed it is
   an identity row (chip · role · Details + chevron) over its metadata row (model, harness chip with the run count,
   cost, last update, docked Reviews count). Only its narration line waits for
   expansion. The title stays on one line and stays IN the identity row at every
   width (in the narrow regime above it ellipsizes before the side controls
   would leave the row). Activity now stays visible beneath it. */
.chat-live-card.subagent .chat-live-title {
    font-weight: 400;
    color: var(--text-secondary);
}

.chat-live-card.subagent .chat-live-activity {
    color: var(--text-secondary);
}

/* A child's report names its reader before its words: an inline label, semibold at the
   row's own size and ink (DESIGN §5), like the timeline heading labels below.
   Drawn from data-lead so the line's fit counts it. */
.chat-live-activity[data-lead]:not(:empty)::before {
    content: attr(data-lead) " · ";
    font-weight: 600;
}

.chat-live-card.subagent:not([data-expanded="1"]) > .chat-live-summary-button .chat-live-title {
    display: block;
    min-height: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chat-live-card.subagent:not([data-expanded="1"]) > .chat-live-summary-button .chat-live-activity {
    -webkit-line-clamp: 1;
    line-clamp: 1;
}
.chat-live-card.subagent .chat-live-meta:empty {
    display: none;
}

.chat-live-card.subagent:not([data-expanded="1"]) > .chat-live-foot > .chat-live-meta {
    min-height: 0;
}

.chat-live-line {
    border-top: 1px solid rgba(var(--neutral-rgb), 0.05);
    padding-top: 10px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.chat-live-line:first-child {
    border-top: none;
    padding-top: 0;
}

.chat-live-line.expandable {
    /* The whole non-interactive row surface toggles the line (v6.71.0). */
    cursor: pointer;
    border-radius: var(--radius-inner);
    transition: background 0.18s ease;
}

.chat-live-line.expandable:hover {
    background: rgba(var(--neutral-rgb), 0.02);
}

.chat-live-line.expandable[data-expanded="1"] {
    background: var(--surface-tray);
}

.chat-live-line-toggle {
    user-select: text;
    -webkit-user-select: text;
    width: 100%;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    cursor: pointer;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    text-align: left;
}

.chat-live-line-toggle:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}

/* The row's title, then its repeat count and its time in columns of their own on the title's first line, so a title of
   two lines never pushes its time onto a third (U017). */
.chat-live-line-head {
    display: grid;
    grid-auto-flow: column;
    grid-template-columns: minmax(0, 1fr);
    grid-auto-columns: max-content;
    align-items: baseline;
    column-gap: var(--space-2);
    flex: 1;
    min-width: 0;
}

/* Collapsed timeline rows are a dense log under the card's summary: meta size
   in the quieter ink. An expanded row is reading text again (below). */
.chat-live-line-title {
    color: var(--text-secondary);
    font-size: var(--type-meta);
    font-weight: 400;
    line-height: var(--line-meta);
    white-space: pre-wrap;
}

.chat-live-line-repeat {
    color: var(--text-muted);
    font-size: var(--type-meta);
}

.chat-live-line-time {
    color: var(--text-muted);
    font-size: var(--type-meta);
    white-space: nowrap;
}

.chat-live-line-expand-label {
    color: var(--text-secondary);
    font-size: var(--type-meta);
    white-space: nowrap;
    flex-shrink: 0;
    padding-top: 1px;
}


.chat-live-line-body {
    color: var(--text-secondary);
    /* white-space is set with the other rendered-markdown surfaces, next to the
       chat bubble's pre-wrap default this body has to opt out of. */
    word-break: break-word;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

/* A markdown heading inside a timeline line is a subsection label (DESIGN.md
   §2), never a page title: the row's own size, semibold, inline, no block
   margins. Bold spans take the same semibold weight. */
.chat-live-line-title .md-h1,
.chat-live-line-title .md-h2,
.chat-live-line-title .md-h3,
.chat-live-line-body .md-h1,
.chat-live-line-body .md-h2,
.chat-live-line-body .md-h3 {
    display: inline;
    font-size: inherit;
    font-weight: 600;
    line-height: inherit;
    margin: 0;
}

.chat-live-line-body[data-body-format="text"] { white-space: pre-wrap; }

.chat-live-line-title strong,
.chat-live-line-body strong {
    font-weight: 600;
}

/* P3: the genuinely-full fetched output is shown in a bounded, scrollable box so a
   huge subagent/research result expands in place without growing the chat endlessly. */
.chat-live-line-body.chat-live-line-body-full {
    max-height: 420px;
    overflow-y: auto;
}

/* A delegated run's observation (#1350): the executor's words are the row's reading
   text in primary ink; thinking, counts, gaps and notes stay in the row's quieter ink.
   Problems use the warn pair — a failed step is not a failed task. */
.chat-delegated-activity > * + * {
    margin-top: var(--space-1);
}

.chat-delegated-message {
    color: var(--text-primary);
}

.chat-delegated-actor {
    display: block;
    color: var(--text-muted);
}

.chat-delegated-problem {
    color: var(--status-warn-fg);
}

/* A disclosure among quieter notes reads as a control: control ink, native marker. */
.chat-delegated-thinking > summary,
.chat-delegated-answer > summary {
    cursor: pointer;
    color: var(--text-primary);
}

.chat-delegated-thought + .chat-delegated-thought {
    margin-top: var(--space-1);
}

.chat-delegated-gap,
.chat-delegated-note,
.chat-delegated-seq {
    color: var(--text-meta);
}

/* A range the wait could not read before returning stays in the warn pair. */
.chat-delegated-final {
    color: var(--status-warn-fg);
}

/* The voice's link ink (DESIGN seed), never the browser default, which falls under 4.5:1 on the dark card. */
.chat-delegated-source {
    color: var(--accent-light);
    overflow-wrap: anywhere;
}

/* Ordered by the journal; each entry already carries its seq, so no list numbers. */
.chat-delegated-events {
    margin: 0;
    padding-inline-start: 0;
    list-style: none;
}

.chat-live-line-loading {
    margin-top: 6px;
    font-size: var(--type-meta);
    font-style: italic;
    color: var(--text-meta);
}

.chat-live-line[data-expanded="1"] .chat-live-line-title {
    color: var(--text-primary);
    font-size: var(--type-body);
    line-height: var(--line-body);
}

.chat-live-line[data-expanded="1"] .chat-live-line-body {
    color: var(--text-primary);
    font-size: var(--type-body);
    line-height: var(--line-body);
}

.chat-live-line.error .chat-live-line-title,
.chat-live-line.timeout .chat-live-line-title {
    color: var(--status-error-fg);
}

/* The evidence row says its phase in ink, not in words: the counts-only label
   stays the same while a call is still in flight (calling: the row is what the
   turn is doing right now, so primary ink, never an alarm hue) and once one
   failed (warn). Colour only, like the error rule above. */
.chat-live-line.calling .chat-live-line-title {
    color: var(--text-primary);
}

.chat-live-line.warn .chat-live-line-title {
    color: var(--status-warn-fg);
}

.chat-bubble .message pre {
    background: rgba(0, 0, 0, 0.3);
    border-radius: 6px;
    padding: 8px 10px;
    margin: 6px 0;
    overflow-x: auto;
    font-size: var(--type-body);
    line-height: var(--line-body);
}
.ui-rich-content .inline-code,
.ui-rich-content .md-code-block code,
.chat-bubble .message code {
    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
    font-size: var(--type-body);
}
/* Code keeps the ink of the text around it: in the accent-tinted assistant
   bubble the link colour made a code span read as a link or an error. The
   neutral pill alone marks it as code. */
.ui-rich-content .inline-code,
.chat-bubble .message .inline-code {
    background: rgba(var(--neutral-rgb), 0.08);
    padding: 1px 5px;
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    color: inherit;
    font-size: var(--type-body);
}
/* A code span is literal: a path or a command keeps its exact spacing on screen and when copied. */
.inline-code { white-space: pre-wrap; }
.chat-bubble .message del {
    opacity: 0.5;
    text-decoration: line-through;
}
.chat-bubble .message p {
    margin: 0 0 var(--space-3);
}
.chat-bubble .message ul,
.chat-bubble .message ol {
    margin: var(--space-2) 0 var(--space-3);
    padding-inline-start: var(--space-5);
}
.chat-bubble .message li { margin: var(--space-1) 0; }
.ui-rich-content li.task-list-item,
.chat-bubble .message li.task-list-item {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    margin-inline-start: calc(-1 * var(--space-5));
    list-style: none;
}
.ui-rich-content .task-list,
.chat-bubble .message .task-list { list-style: none; }
.md-checkbox {
    display: inline-flex;
    width: 18px;
    height: 18px;
    flex: 0 0 18px;
    align-items: center;
    justify-content: center;
    margin-top: 1px;
    border: 1px solid var(--field-border);
    border-radius: var(--radius-inner);
    background: var(--surface-tray);
    color: var(--status-ok-fg);
    font-size: var(--type-body);
    font-weight: 700;
    line-height: 1;
}
.md-checkbox.is-checked {
    border-color: var(--status-ok-border);
    background: var(--status-ok-bg);
}
.md-quote {
    display: block;
    margin: var(--space-3) 0;
    padding: var(--space-2) var(--space-3);
    border-left: 3px solid var(--accent);
    background: var(--surface-tray);
    color: var(--text-meta);
}
/* The markdown renderer emits .md-table everywhere it runs — live activity
   lines, quiz cards, skill review bodies, review attempt details — but the
   table styling was scoped to the chat bubble, so every other surface showed
   an unseparated run of cells. The class travels with the renderer, like
   .md-h1 / .md-quote / .md-link above. A table keeps its grid and fills its
   column; cells wrap like prose, so it scrolls sideways only when its columns
   cannot fit even wrapped. */
.md-table {
    width: 100%;
    border-collapse: collapse;
    color: var(--text-primary);
    font-size: var(--type-body);
    font-variant-numeric: tabular-nums;
}
.md-table th,
.md-table td {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--divider);
    text-align: start;
    vertical-align: top;
    /* Words stay whole: the bubble's `anywhere` or a timeline row's
       `break-word` would let the table layout squeeze a short column (a
       figure, a name) to a letter per line instead of scrolling. */
    overflow-wrap: normal;
    word-break: normal;
}
/* A link, hash or code span has no space to wrap at, so it may break anywhere
   rather than widen its column past the bubble. */
.md-table :is(a, code) { overflow-wrap: anywhere; }
/* The author's GFM alignment (marked writes it as `align`, which any rule here
   would otherwise override). */
.md-table :is(th, td)[align="left"] { text-align: left; }
.md-table :is(th, td)[align="center"] { text-align: center; }
.md-table :is(th, td)[align="right"] { text-align: right; }
.md-table th {
    font-weight: 600;
    color: var(--text-primary);
}
/* A full rich answer's table reads at the answer's own text size (DESIGN.md
   §5); compact and card tables keep --type-body. */
.chat-bubble .message:where(.ui-rich-content) .md-table { font-size: inherit; }

/* Global markdown rendering classes. */
.md-h1 {
    font-size: var(--type-page);
    color: var(--text-primary);
    display: block;
    margin: var(--space-5) 0 var(--space-3);
    line-height: var(--line-title);
}
.md-h2 {
    font-size: var(--type-section);
    color: var(--text-primary);
    display: block;
    margin: var(--space-4) 0 var(--space-2);
    line-height: var(--line-title);
}
.md-h3 {
    font-size: var(--type-body);
    color: var(--text-primary);
    display: block;
    margin: var(--space-3) 0 var(--space-2);
    line-height: var(--line-title);
}
/* Rich content uses subsection labels at every mount, including cards. */
.ui-rich-content :is(.md-h1, .md-h2, .md-h3) {
    font-size: var(--type-body);
    color: var(--text-primary);
    font-weight: 600;
    margin-block: var(--space-3) var(--space-2);
}
/* Compact Markdown in a bubble (a Skill Review report) keeps its dense
   labels (DESIGN.md §2): every heading level at --type-body semibold, in the
   transcript's established rhythm. */
.chat-bubble .message .md-h1,
.chat-bubble .message .md-h2,
.chat-bubble .message .md-h3 {
    font-size: var(--type-body);
    font-weight: 600;
}
.chat-bubble .message .md-h1 { margin-block: var(--space-5) var(--space-3); }
.chat-bubble .message .md-h2 { margin-block: var(--space-4) var(--space-2); }
.chat-bubble .message .md-h3 { margin-block: var(--space-3) var(--space-2); }
/* Chat reading ladder (DESIGN.md §1, §5), for a full rich answer only: the
   bubble whose message chat.js marks `.ui-rich-content`. A heading there is a
   section of the answer, never a page title, and never smaller than the text
   it opens: `#` = `##`, `###` one step down, deeper levels at the reading
   size; all in the bubble's own ink, with clearly more space above than below.
   `:where` keeps the rich scope weightless, so these rules win over the
   compact ones above by order and the first/last-child rules below still apply.
   A delivered document in the reader reads on the same ladder (DESIGN
   "Document reading"). */
.chat-bubble .message:where(.ui-rich-content) :is(.md-h1, .md-h2, .md-h3),
.document-reader-markdown :is(.md-h1, .md-h2, .md-h3) {
    color: inherit;
    font-weight: 600;
    line-height: var(--line-title);
    text-wrap: balance;
    margin-block: var(--space-5) var(--space-2);
}
.chat-bubble .message:where(.ui-rich-content) :is(.md-h1, .md-h2),
.document-reader-markdown :is(.md-h1, .md-h2) { font-size: var(--md-heading-major); }
.chat-bubble .message:where(.ui-rich-content) .md-h3,
.document-reader-markdown .md-h3 { font-size: var(--md-heading-minor); }
/* h4-h6 carry .md-h3 (the compact demotion); `:where` keeps this rule no
   stronger than the class rule it follows. */
.chat-bubble .message:where(.ui-rich-content) :where(h4, h5, h6).md-h3,
.document-reader-markdown :where(h4, h5, h6).md-h3 {
    font-size: inherit;
    margin-block: var(--space-4) var(--space-1);
}
.chat-bubble .message > :first-child { margin-block-start: 0; }
.chat-bubble .message > :last-child { margin-block-end: 0; }
/* Bold text never outweighs the heading above it. */
.ui-rich-content :is(strong, b),
.chat-bubble .message :is(strong, b) { font-weight: 600; }
/* A thematic break is a quiet section rule, not the browser's inset double line. */
.ui-rich-content hr {
    height: 0;
    border: 0;
    border-top: 1px solid var(--divider);
    margin-block: var(--space-5);
}
.md-li {
    display: block;
    padding-left: 12px;
}
.md-link {
    color: var(--accent-light);
    text-decoration: underline;
    text-underline-offset: 2px;
}
.md-link:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }

/* Prevent wide tables from breaking bubble layout. */
.md-table-wrap {
    display: block;
    width: 100%;
    margin: var(--space-4) 0;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    max-width: 100%;
    scrollbar-color: var(--text-disabled) var(--surface-tray);
    --table-fade-start: 0px;
    --table-fade-end: 0px;
}
/* A table that really scrolls fades on each side that still hides columns, the
   `.scroll-fade-y` idiom turned sideways: `data-scroll-start`/`-end` come from
   `bindMarkdownTables` (chat_markdown.js), shared by the rich enhancement and
   compact surfaces, which also makes that wrapper a focusable region. A mask
   fades the content itself, so it reads on every bubble tint. */
.md-table-wrap[data-scroll-start] { --table-fade-start: var(--space-5); }
.md-table-wrap[data-scroll-end] { --table-fade-end: var(--space-5); }
.md-table-wrap:is([data-scroll-start], [data-scroll-end]) {
    -webkit-mask-image: linear-gradient(to right, transparent 0, black var(--table-fade-start),
        black calc(100% - var(--table-fade-end)), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, black var(--table-fade-start),
        black calc(100% - var(--table-fade-end)), transparent 100%);
}
/* Focused, the whole table and its ring show. */
.md-table-wrap:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
    -webkit-mask-image: none;
    mask-image: none;
}

.md-code-block {
    position: relative;
    width: 100%;
    min-width: 0;
    margin: var(--space-4) 0;
    overflow: hidden;
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    background: var(--code-bg);
    color: var(--text-primary);
}
.ui-rich-content .md-code-block pre,
.chat-bubble .message .md-code-block pre {
    margin: 0;
    padding: var(--space-4);
    overflow: auto;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: inherit;
    font-size: var(--type-body);
    line-height: var(--line-body);
    white-space: pre;
    tab-size: 2;
}
.md-code-language {
    display: block;
    min-height: 34px;
    padding: 9px 76px 0 var(--space-4);
    border-bottom: 1px solid var(--divider);
    color: var(--text-meta);
    font: var(--type-meta)/var(--line-meta) 'SF Mono', 'Fira Code', Consolas, monospace;
}
/* Copy is the shared quiet capsule (.btn.btn-ghost.btn-sm), centred in the language line. A code block is the room's
   material inside any voice, so a hovering pointer gives Copy the room's fill, not the voice's. */
.md-code-copy {
    position: absolute;
    z-index: 1;
    top: 3px;
    right: var(--space-2);
}
.md-code-copy.is-copied { color: var(--status-ok-fg); }

.md-code-block .hljs-comment,
.md-code-block .hljs-quote { color: #8b8294; font-style: italic; }
.md-code-block .hljs-keyword,
.md-code-block .hljs-selector-tag,
.md-code-block .hljs-literal { color: #d29ce8; }
.md-code-block .hljs-string,
.md-code-block .hljs-regexp,
.md-code-block .hljs-addition { color: #8dd7c7; }
.md-code-block .hljs-number,
.md-code-block .hljs-built_in { color: #ee967a; }
.md-code-block .hljs-title,
.md-code-block .hljs-function { color: #92b7f4; }
.md-code-block .hljs-type,
.md-code-block .hljs-class .hljs-title { color: #e5c66d; }
.md-code-block .hljs-attr,
.md-code-block .hljs-variable,
.md-code-block .hljs-template-variable { color: #add782; }

.ui-rich-content .katex,
.chat-bubble .message .katex { color: var(--text-primary); }
.ui-rich-content .katex-display,
.chat-bubble .message .katex-display {
    max-width: 100%;
    margin: var(--space-4) 0;
    padding: var(--space-1) 0;
    overflow-x: auto;
    overflow-y: hidden;
}

.md-mermaid,
.md-chart {
    box-sizing: border-box;
    width: 100%;
    margin: var(--space-4) 0;
    padding: var(--space-4);
    overflow: auto;
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    background: var(--diagram-bg);
    color: var(--text-primary);
}
.md-mermaid { text-align: center; }
.md-mermaid-stage {
    position: fixed;
    top: 0;
    left: -10000px;
    width: var(--md-mermaid-stage-width, 320px);
    opacity: 0;
    pointer-events: none;
}
.md-mermaid svg {
    display: block;
    width: auto;
    min-width: min-content;
    max-width: 100%;
    height: auto;
    margin: 0 auto;
}
.md-chart {
    height: clamp(280px, 42vw, 480px);
    overflow: hidden;
}
.md-chart canvas {
    display: block;
    width: 100% !important;
    height: 100% !important;
}
.md-chart-error {
    height: auto;
    overflow-x: auto;
    border-color: var(--status-error-fg);
    color: var(--text-meta);
    font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
    font-size: var(--type-meta);
    line-height: var(--line-body);
    text-align: left;
    white-space: pre-wrap;
}
.md-mermaid-error { border-color: var(--status-error-fg); }
.md-diagram-error-note {
    padding: var(--space-2) var(--space-4);
    border-bottom: 1px solid var(--divider);
    color: var(--status-error-fg);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

/* A touch keyboard zooms into a field under 16px: the guard follows the pointer, not a width. Below 100 % interface
   size (theme.js) the field keeps 16 device px, so a smaller page never makes the keyboard zoom in. */
@media (pointer: coarse) {
    textarea, select, input:not([type]), [contenteditable]:not([contenteditable="false"]),
    input:is([type="text"], [type="search"], [type="email"], [type="url"], [type="tel"], [type="password"], [type="number"]) {
        font-size: max(16px, calc(16px / var(--interface-scale, 1))) !important;
    }
}

/* The stack layout (html[data-layout="stack"], written by theme.js before first paint; layout.js): the sidebar
   is a drawer, every room a full-width layer. Bubble width is not set here: the `chatcol` container rule is the
   one narrow-column rule, for a phone and a narrow Project panel alike. */

html[data-layout="stack"] #app {
    grid-template-columns: minmax(0, 1fr);
}
html[data-layout="stack"] .mobile-nav-toggle {
    position: relative;  /* its status dot sits on its corner */
    flex: 0 0 44px;
    display: inline-flex;
}
/* The ☰'s dot says what waits behind it (app.js paintNavToggles): the shared 7px fact dot on the control's top-right,
   clear of the glyph's top stroke, with no ring around it; inset closer on the composer's 32px capsule. */
.mobile-nav-toggle > .status-mark {
    position: absolute;
    top: 8px;
    right: 8px;
}
.mobile-nav-toggle.composer-menu > .status-mark {
    top: 3px;
    right: 3px;
}
/* A room's ☰ is the first control of its composer's row, under the thumb (owner 19.10, 1A): as tall as the
   capsules beside it, with the touch minimum as its finger target (.ui-hit-area). */
html[data-layout="stack"] .chat-toolbar-row > .composer-menu {
    position: relative;  /* its hit area (.ui-hit-area::after) is centred on it, not on the composer */
    flex: 0 0 auto;
    width: var(--composer-control-height);
    height: var(--composer-control-height);
}
html[data-layout="stack"] .chat-toolbar-row > .composer-menu svg {
    width: 18px;
    height: 18px;
}
/* The drawer on a phone (a layer, layers.js; owner 19.10, 1A): the room is a card the drawer pushes aside. The same
   sidebar lies under the room; opening slides the room right by the drawer's width, three quarters of the screen and
   never more than 360px (--drawer-w), as a card (the screen's corners, a soft shadow, its words faded under a veil),
   and brings the sidebar up from under it; the quarter of the room left showing is the way back. Nothing darkens.
   Its head row holds the one × exactly where a page header's ☰ is (the header's inset and leading slot), so the
   control the finger reached for stays under it; a room's ☰ is its composer's first control, and the × stays here
   (owner 19.10, 2A). */
html[data-layout="stack"] {
    --drawer-w: min(75vw, 360px);
    --room-card-radius: 44px;
    --room-card-shadow: 0 0 0 0.5px rgba(var(--neutral-rgb), 0.12), -12px 0 40px rgba(0, 0, 0, 0.5);
}
html[data-theme="light"][data-layout="stack"] {
    --room-card-shadow: 0 0 0 0.5px rgba(var(--neutral-rgb), 0.1), -10px 0 40px rgba(17, 20, 32, 0.16);
}
/* The scrim is the way back: clear, and only over the room's visible quarter (the drawer, under the room, keeps its
   own touches). */
html[data-layout="stack"] .nav-drawer-backdrop {
    position: fixed;
    inset: 0 0 0 var(--drawer-w);
    z-index: var(--z-drawer-scrim);
    display: block;
    background: transparent;
    /* It comes with the drawer: shown at once (from 0, @starting-style) and put away by app.js after the close;
       while it goes it is not a target. */
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--drawer-settle, var(--motion-close)) ease;
    touch-action: pan-y pinch-zoom;  /* a sideways finger on the scrim drags the drawer closed (drawer_drag.js) */
}
html[data-layout="stack"] body.nav-drawer-open .nav-drawer-backdrop {
    opacity: 1;
    pointer-events: auto;
    transition-duration: var(--drawer-settle, var(--motion-open));
}
@starting-style {
    html[data-layout="stack"] body.nav-drawer-open .nav-drawer-backdrop { opacity: 0; }
}
html[data-layout="stack"] .nav-drawer-backdrop[hidden] {
    display: none;
}
html[data-layout="stack"] #primary-sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: var(--z-sidebar);  /* under the room: the room is the card that moves */
    width: var(--drawer-w);
    /* Closed it lies under the room a little to the left and faded (p = 0), and comes up from there as the room
       slides away; it closes in 280ms and opens in 340ms on the iOS sheet curve, or at the finger's speed after a
       drag (--drawer-settle, drawer_drag.js). */
    transform: translateX(-16%);
    opacity: 0.5;
    transition: transform var(--drawer-settle, var(--motion-close)) var(--ease-sheet),
        opacity var(--drawer-settle, var(--motion-close)) ease;
    --sidebar-pad-bottom: calc(var(--space-3) + var(--inset-bottom));
    padding-bottom: var(--sidebar-pad-bottom);
    touch-action: pan-y;  /* a sideways finger is the drawer's (drawer_drag.js); up and down scroll the list */
}
/* The same on the room: a page's vertical scroller hands a sideways finger to the drawer. touch-action starts over
   at every scroller, so it is declared on the scrollers themselves; a code block or a table inside scrolls sideways
   as its own scroller. */
html[data-layout="stack"] :is(#chat-messages, .chat-messages, .files-list, .files-sidebar, .files-preview-content,
    .skills-scroll, .widgets-scroll, .settings-scroll, .dashboard-body) {
    touch-action: pan-y pinch-zoom;
}
/* Outside the scrollers too (a header, the composer's row of capsules, the attach button): the page itself only pans
   up and down and zooms, so a sideways finger anywhere on the room is the drawer's, from the screen's edge on. */
html[data-layout="stack"] body {
    touch-action: pan-y pinch-zoom;
}
html[data-layout="stack"] .sidebar-head {
    padding-top: 0;
}
html[data-layout="stack"] .drawer-head {
    display: flex;
    align-items: center;
    min-height: 56px;  /* the header row's height: the × lands exactly on the ☰ */
    padding: calc(var(--chrome-inset) + var(--inset-top)) 0 var(--chrome-inset);
}
/* Pulled past fully open the room gives a little (a rubber band, drawer_drag.js, at most DRAG.rubberMax = 48px) and
   the drawer goes with it; the drawer's surface reaches 64px past its left edge as a solid shadow, so the screen's
   edge never shows through the gap: a shadow paints outside the box, where the drawer's own scroller would clip a
   child or a pseudo-element. */
html[data-layout="stack"] #primary-sidebar.open {
    transform: none;
    opacity: 1;
    box-shadow: -64px 0 0 var(--surface-side);
    transition-duration: var(--drawer-settle, var(--motion-open));
}
/* Under a finger (drawer_drag.js) everything that moves follows one number, --drawer-p (0 closed … 1 open), with no
   transition: the drawer comes up from under the room, the room slides as the card, its veil fades its words.
   Releasing drops the class, and they settle the rest of the way at the finger's speed; reduced motion keeps the
   finger tracking and drops the settle. Past open the drawer follows the room's give (the larger of the two
   offsets). */
html[data-layout="stack"] #primary-sidebar.dragging {
    transition: none;
    will-change: transform;
    transform: translate3d(max(calc((var(--drawer-p, 0) - 1) * 0.16 * var(--drawer-w)),
        calc((var(--drawer-p, 0) - 1) * var(--drawer-w))), 0, 0);
    opacity: calc(0.5 + 0.5 * min(var(--drawer-p, 0), 1));
    box-shadow: -64px 0 0 var(--surface-side);
}
html[data-layout="stack"] body .nav-drawer-backdrop.dragging {
    opacity: var(--drawer-p, 0);
    transition: none;
}
/* The room (Main, or a Project open over it) is the card: opaque, so the drawer under it never shows through, and
   pushed aside from its left edge (each room's own stack rule: transform-origin 0 50%). */
html[data-layout="stack"] body.nav-drawer-open :is(#content, .project-panel) {
    transform: translateX(var(--drawer-w)) scale(0.96);
    border-radius: var(--room-card-radius);
    box-shadow: var(--room-card-shadow);
    overflow: hidden;
    transition-duration: var(--drawer-settle, var(--motion-open));
}
/* Under a finger. `body` makes it as specific as the open card's rule above, so a finger that takes the open drawer
   back, or pulls it past open, moves the card too; the veil's rule below likewise. */
html[data-layout="stack"] body :is(#content, .project-panel.open).dragging {
    transition: none;
    will-change: transform;
    transform: translate3d(calc(var(--drawer-p, 0) * var(--drawer-w)), 0, 0) scale(calc(1 - 0.04 * min(var(--drawer-p, 0), 1)));
    border-radius: calc(var(--room-card-radius) * min(var(--drawer-p, 0), 1));
    box-shadow: var(--room-card-shadow);
    overflow: hidden;
}
/* The room's words fade under one veil: an element of its own, because --drawer-p is not inherited (one box per
   frame), so a pseudo-element of the card could not follow the finger. It lies over everything in the room. */
.room-veil {
    display: none;
}
html[data-layout="stack"] .room-veil {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 100;  /* the top of the room's own stack, which the room's z-index keeps under every shell layer */
    pointer-events: none;
    background: var(--bg-primary);
    opacity: 0;
    transition: opacity var(--drawer-settle, var(--motion-close)) ease;
}
html[data-layout="stack"] body.nav-drawer-open .room-veil {
    opacity: 0.42;
    transition-duration: var(--drawer-settle, var(--motion-open));
}
html[data-layout="stack"] body .room-veil.dragging {
    opacity: calc(0.42 * min(var(--drawer-p, 0), 1));
    transition: none;
}
@media (prefers-reduced-motion: reduce) {
    html[data-layout="stack"] :is(#primary-sidebar, #primary-sidebar.open, .nav-drawer-backdrop, #content,
        .project-panel, .room-veil) { transition: none; }
}
/* A place chosen in the open drawer changes inside the card (shell_motion.js; owner 20.10, 4A). The page that leaves
   stays displayed for the beat, out of the flow, while its words fade; a Project room that leaves lies over the
   arriving one inside the panel. The Project panel is the card whether arriving or leaving while the drawer is painted
   open (above), so it never rises over the drawer. */
#content > .page[data-leaving] {
    display: flex;
    position: absolute;
    inset: 0;
    pointer-events: none;
}
.chat-instance-panel[data-leaving] {
    position: absolute;
    inset: 0;
    pointer-events: none;
}
/* Main's words fade under a cover of its own background (shell_motion.js `fade`): the page is its stacking context,
   so the cover lies over everything in it. */
html[data-layout="stack"] #page-chat { isolation: isolate; }
html[data-layout="stack"] #page-chat > .shell-cover {
    position: absolute;
    inset: 0;
    z-index: 2147483647;
    background: var(--bg-primary);
    opacity: 0;  /* at rest it covers nothing: a fade that ends without holding its last frame leaves the room seen */
    pointer-events: none;
}
/* Under the closed drawer the room card hides its rows: their moving marks rest instead of running unseen (most of a
   page's running animations), and start again with the drawer, in the page's one phase (status_mark.js). */
html[data-layout="stack"] #primary-sidebar:not(.open):not(.dragging) {
    --mark-wave: none;
}
/* A tap on a drawer row answers with its colour at once (a press is colour, never a sink): without a hover the rows
   had only iOS's grey flash. */
.nav-row,
.nav-section-toggle {
    -webkit-tap-highlight-color: transparent;
}
.nav-row:not(.active):active,
.nav-section-toggle:active {
    background: var(--tone-quiet-hover);
}
html[data-layout="stack"] .unread-badge {
    min-width: 14px;
    height: 14px;
}
html[data-layout="stack"] #content {
    transform-origin: 0 50%;
    grid-column: 1;
    padding-left: 0;
    padding-bottom: 0;
    z-index: var(--z-room);
    background: var(--bg-primary);
    transition: transform var(--drawer-settle, var(--motion-close)) var(--ease-sheet),
        border-radius var(--drawer-settle, var(--motion-close)) var(--ease-sheet),
        box-shadow var(--drawer-settle, var(--motion-close)) ease;
}
html[data-layout="stack"] #content > .page:not(#page-chat) {
    padding-bottom: var(--inset-bottom);
}
/* The stack layout's header is one row of at most 56px at normal scale: `☰ · title · status … chip · Panic`
   (owner Q4=A; no ⋯, owner 25.10). Nothing wraps: the title ellipsizes, Restart and the budget are rows of the room
   status's Shade (page_header.js places them). */
html[data-layout="stack"] .page-header,
html[data-layout="stack"] .app-page-header {
    min-height: 56px;
    padding: var(--chrome-inset) var(--space-3);
    padding-top: calc(var(--chrome-inset) + var(--inset-top));
    column-gap: var(--space-2);
}
html[data-layout="stack"] .chat-page-header {
    flex-wrap: nowrap;
}
html[data-layout="stack"] .chat-page-header > .app-page-title-block {
    flex: 0 1 auto;
    min-width: 0;
}
/* The actions keep their own width; the title block takes the rest (the status after the title ellipsizes last). */
html[data-layout="stack"] .chat-page-header > .app-page-actions {
    flex: 0 0 auto;
}
html[data-layout="stack"] .chat-page-note {
    left: var(--space-3);
    max-width: calc(100% - 2 * var(--space-3));
    padding: 2px 0 var(--space-2);
}
#chat-input-area,
.chat-input-area {
    /* Bottom fade darkens transcript before the frosted input without trapping scroll. */
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 24px 14px max(16px, var(--inset-bottom));
    align-items: stretch;
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 5;
    pointer-events: none;
    background: linear-gradient(
        0deg,
        rgba(var(--shell-rgb), 0.72) 0%,
        rgba(var(--shell-rgb), 0.36) 48%,
        rgba(var(--shell-rgb), 0.10) 78%,
        rgba(var(--shell-rgb), 0.00) 100%
    );
}

#chat-input-area > *,
.chat-input-area > * {
    pointer-events: auto;
}

.chat-input-wrap {
    --composer-control-font-size: 12px;
    flex: 1;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    min-width: 0;
}

/* The field (DESIGN "The composer field"): one floating object holding a staged reply's strip and the text row. Its
   corner is the sheet radius, half its one-line height, and never grows with the words: one line is a capsule, a
   paragraph a card with the same corner, never an oval. No border: its edge and its focus ring are shadows, so the
   one-line field is exactly the row's height. A transparent outline inside the edge is its edge where shadows are
   not drawn (Windows High Contrast paints it in the system colour), wider while the field holds the focus. */
.chat-field {
    order: 2;
    display: flex;
    flex-direction: column;
    min-width: 0;
    border-radius: var(--radius-sheet);
    outline: 1px solid transparent;
    outline-offset: -1px;
    background: var(--surface-float);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    box-shadow: var(--elev-float);
    transition: box-shadow 0.2s, background 0.2s;
}

.chat-field:focus-within {
    outline-width: 2px;
    outline-offset: -2px;
    background: var(--surface-float);
    box-shadow: 0 0 0 1px var(--focus-accent-border), 0 0 0 4px var(--focus-accent-ring), var(--elev-float);
}

/* "+", the words, the mic and Send: the controls ride on the words' last line (flex-end), never on their middle. */
.chat-text-row {
    display: flex;
    align-items: flex-end;
    gap: var(--space-2);
    min-width: 0;
    padding: var(--space-2);
}

/* "Chat freed" cue (P3): a brief fuchsia brighten when a task moves out to a
   project, signalling the main chat is free again. The animation owns the
   900ms and the class comes off on animationend, so no chat-instance timer. */
.chat-field.chat-freed { animation: chat-freed 900ms ease-out; }
@keyframes chat-freed {
    0%, 70% {
        box-shadow: inset 0 1px 0 rgba(var(--neutral-rgb), 0.04), 0 0 0 1px var(--project), 0 0 0 4px var(--project-12);
    }
}

.chat-toolbar-row {
    order: 1;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: wrap;
    --composer-row-gap: var(--space-2);
    gap: var(--composer-row-gap);
    min-width: 0;
    padding: 0 var(--space-2);
    /* Size containment: the row's width never depends on what it holds, so the open effort
       strip's measured fit (composer_owner_controls.js) cannot feed back into it. */
    container: composer / inline-size;
}

#chat-input-area.drag-active .chat-input-wrap,
.chat-input-area.drag-active .chat-input-wrap {
    border-radius: 24px;
    box-shadow: 0 0 0 1px var(--accent-45), 0 0 0 4px var(--accent-10);
}

/* "+": the composer's actions menu, a flow item at the left of the text row (no absolute float). A finger-sized
   target whose negative margin keeps the row's height and the field's place. */
.chat-plus-btn {
    flex: 0 0 auto;
    align-self: flex-end;
    width: var(--touch-target-min);
    height: var(--touch-target-min);
    margin: calc((var(--composer-control-height) - var(--touch-target-min)) / 2);
    border-radius: var(--pill-radius);
    border: none;
    background: transparent;
    color: var(--text-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: color 0.15s, background 0.15s;
    padding: 0;
}

.chat-plus-btn:hover,
.chat-plus-btn[aria-expanded="true"] {
    color: var(--text-primary);
    background: rgba(var(--neutral-rgb), 0.06);
}

.chat-plus-btn:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }

.chat-file-input-hidden {
    display: none !important;
}

.chat-plus-btn.uploading {
    opacity: 0.5;
    pointer-events: none;
}

/* The "+" menu is the shared menu sheet (ui.css): an item's icon in the secondary ink, a section under its label. */
.chat-plus-menu .chat-plus-item { display: flex; align-items: center; gap: var(--space-2); }
.chat-plus-icon { display: inline-flex; flex: 0 0 auto; color: var(--text-secondary); }
.chat-plus-group { display: flex; flex-direction: column; }
.chat-plus-label {
    padding: var(--space-2) var(--space-3) var(--space-1);
    color: var(--text-meta);
    font-size: var(--type-meta);
    font-weight: 500;
}

/* Mount points (empty and hidden until a control fills them): the dictation mic between the field and Send, and
   contributed chips after the room's capsules on their one line. The host capsules keep priority: the chips slot
   takes only the room they leave (a zero basis never wraps the row; it shrinks first and clips), and a chip keeps
   its own width. */
.chat-voice-slot { display: inline-flex; flex: 0 0 auto; align-self: flex-end; align-items: center; gap: var(--space-1); }
/* The dictation mic and its recording face (voice_controls.js): quiet finger-sized controls like "+", whose negative
   margin keeps the row's height; the chevron is a narrow, finger-high door to the engine menu. While recording, Stop
   wears the soft crimson of a control that is on now and the dot follows the microphone's level. */
.voice-btn {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-target-min);
    height: var(--touch-target-min);
    margin: calc((var(--composer-control-height) - var(--touch-target-min)) / 2) 0;
    padding: 0;
    border: none;
    border-radius: var(--pill-radius);
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
    transition: color 0.15s, background 0.15s;
}
.voice-btn:hover { color: var(--text-primary); background: rgba(var(--neutral-rgb), 0.06); }
.voice-btn:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
.voice-btn:disabled { color: var(--text-disabled); background: transparent; cursor: default; }
.voice-chevron { width: 24px; margin-left: calc(-1 * var(--space-1)); }
.voice-stop:not(:disabled), .voice-stop:not(:disabled):hover { background: var(--tone-crimson-soft); color: var(--tone-crimson-soft-ink); }
.voice-time, .voice-status {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--text-secondary);
    font-size: var(--composer-control-font-size);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.voice-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--accent-light);
    transform: scale(calc(0.85 + var(--voice-level, 0) * 0.65));
}
.chat-voice-slot[data-voice-state="permission"] .voice-dot { opacity: 0.4; }
/* Dictated words whose draft changed meanwhile wait above the composer as a float sheet until the owner puts them in,
   copies or discards them; the draft below stays as it is. */
.voice-offer {
    position: absolute;
    right: 0;
    bottom: calc(100% + var(--space-2));
    z-index: 5;
    box-sizing: border-box;
    width: min(100%, 420px);
    padding: var(--space-3);
    border-radius: var(--radius-object);
    background: var(--surface-float);
    color: var(--text-primary);
    box-shadow: var(--elev-float);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
}
.voice-offer-note { margin: 0 0 var(--space-1); color: var(--text-meta); font-size: var(--type-meta); }
.voice-offer-text { margin: 0; max-height: 6.5em; overflow-y: auto; overflow-wrap: anywhere; white-space: pre-wrap; }
.voice-offer-actions { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); }
.voice-offer-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-target-min);
    height: var(--touch-target-min);
    margin-left: auto;
    padding: 0;
    border: none;
    border-radius: var(--pill-radius);
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
}
.voice-offer-close:hover { background: var(--tone-quiet-hover); color: var(--text-primary); }
/* Engine and voice menus: the shared menu sheet, an item's name over its destination line in the meta ink. */
.voice-menu { width: min(360px, var(--ui-popup-max-width)); }
.ui-popup .voice-menu-item { display: flex; flex-direction: column; gap: 2px; }
.ui-popup .voice-menu-item[aria-checked="true"] { background: var(--accent-12); }
.voice-menu-item[aria-checked="true"] .voice-menu-name { color: var(--accent-light); }
.voice-menu-name { font-weight: 500; }
.voice-menu-line, .voice-menu-note { color: var(--text-meta); font-size: var(--type-meta); }
.voice-menu-hint { color: var(--accent-light); font-size: var(--type-meta); font-weight: 600; }
.voice-menu-head { padding: var(--space-2) var(--space-3) var(--space-1); color: var(--text-meta); font-size: var(--type-meta); font-weight: 500; }
.voice-menu-note { padding: 0 var(--space-3) var(--space-2); }
.chat-composer-chips {
    display: flex;
    flex: 1 1 0;
    align-items: center;
    gap: 6px;
    min-width: 0;
    max-width: max-content;
    /* The room before the first chip is the slot's own (an empty first item and the gap after it): it gives the row's
       gap back, so a line the capsules fill keeps the slot on it at no width ("roomless") instead of wrapping it onto
       a second line, and the capsules' fit never counts it (composer_owner_controls.js). */
    margin-inline-start: calc(-1 * var(--composer-row-gap));
    overflow: hidden;
}
.chat-composer-chips::before { content: ''; flex: none; width: calc(var(--composer-row-gap) - 6px); }
.chat-composer-chips > * { flex: 0 0 auto; }
/* Where the engine can clip one axis, the slot clips only sideways, so the chips keep their rest shadow below them.
   Folded, it takes all the room the capsules leave, and "+N" lists the chips that do not fit (composer_chips.js). */
@supports (overflow: clip) {
    .chat-composer-chips { overflow-x: clip; overflow-y: visible; }
}
.chat-composer-chips[data-folded] { max-width: none; }
/* No room even for "+N" (composer_chips.js lists the chips in "+"): the empty slot keeps the row's height, so its
   top stays with the capsules, and still takes the room they leave, so a wider row brings the chips back. */
.chat-composer-chips[data-roomless] { align-self: stretch; }
/* A contributed chip is one of the row's capsules: the pills' height, the skill's name in the secondary ink at 500,
   its value in the meta ink at 600, and a dot of the value's status tone when the skill names one. */
/* The composer (DESIGN.md design bible): Subagents, Context and Effort are neutral capsules on glass. A setting's
   name takes the secondary ink at 500 and its value the meta ink at 600 (owner 16.10: never the darkest ink); the only
   colour is a ring's lit arc or the lit helpers of Subagents' network, Swarm included (owner 28.10: no red fill). Send
   is the primary action's crimson tone. */
.chat-composer-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    box-sizing: border-box;
    height: var(--composer-control-height);
    padding: 0 12px;
    border: 1px solid transparent;
    border-radius: 999px;
    background: var(--surface-capsule);
    color: var(--text-secondary);
    box-shadow: var(--elev-rest);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    font: inherit;
    font-size: var(--composer-control-font-size);
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    transition: var(--motion-tone);
}
.chat-composer-chip:hover { background: var(--surface-capsule-hover); color: var(--text-primary); }
.chat-composer-chip:not(:disabled):active { filter: var(--press); }
.chat-composer-chip:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: -2px; }
.chat-chip-name { font-weight: 500; }
.chat-chip-value { color: var(--text-meta); font-weight: 600; }
.chat-chip-dot { display: none; flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%; }
.chat-composer-chip:is([data-tone="ok"], [data-tone="warn"], [data-tone="bad"]) .chat-chip-dot { display: inline-block; }
.chat-composer-chip[data-tone="ok"] .chat-chip-dot { background: var(--status-ok-fg); }
.chat-composer-chip[data-tone="warn"] .chat-chip-dot { background: var(--status-warn-fg); }
.chat-composer-chip[data-tone="bad"] .chat-chip-dot { background: var(--status-error-fg); }
/* "+N" is the narrowest capsule, so it still fits the room a 320 px phone's capsules leave. */
.chat-chip-more { padding: 0 var(--space-2); }

.chat-attachment-preview {
    display: none;
    padding: 4px 8px 2px;
    max-height: 112px;
    overflow-y: auto;
}

.chat-attachment-preview.visible {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
}

.attach-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 8px 4px 6px;
    background: rgba(var(--composer-rgb), 0.78);
    border: 1px solid var(--accent-35);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-radius: var(--radius-inner);
    font-size: var(--type-meta);
    color: var(--text-primary);
    max-width: 100%;
}

.attach-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: min(220px, 56vw);
}

.attach-remove {
    flex-shrink: 0;
    border: none;
    background: none;
    color: var(--text-secondary);
    cursor: pointer;
    font-size: var(--type-section);
    line-height: 1;
    padding: 0 2px;
}

.attach-remove:hover {
    color: var(--text-primary);
}

#chat-input,
textarea.chat-input {
    /* Bare text surface inside the field (.chat-field owns the surface and the
       focus ring), so no fixed side paddings to reserve room for floated
       controls — the row flexes at any width. */
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
    padding: 5px 4px;
    border: none;
    background: transparent;
    box-shadow: none;
    color: var(--text-primary);
    font-size: var(--type-section);
    font-family: inherit;
    outline: none;
    resize: none;
    /* Grows with its text or placeholder to this one cap, then scrolls;
       chat_input_sizing.js picks content sizing, else writes the measured height. */
    height: var(--chat-input-height, auto);
    /* Text wraps, so the composer never scrolls sideways; a sub-pixel width must not draw a bar. */
    overflow-x: hidden;
    min-height: 32px;
    max-height: var(--message-field-max-height);
    line-height: 1.45;
}
#chat-input[data-sizing="content"],
textarea.chat-input[data-sizing="content"] { field-sizing: content; }
/* A measured box below the cap holds its whole text: a rounded scrollHeight must not
   leave a sub-pixel scrollbar. */
#chat-input[data-sizing="fit"],
textarea.chat-input[data-sizing="fit"] { overflow-y: hidden; }
#chat-input::placeholder,
textarea.chat-input::placeholder { color: var(--text-muted); }

.chat-send-group {
    flex: 0 0 auto;
    align-self: flex-end;
    display: flex;
    align-items: center;
    gap: 0;
    height: var(--composer-control-height);
}

.chat-send-group[data-busy="1"] {
    opacity: 0.72;
}

.chat-send-inline {
    --press: var(--press-solid);
    border: 1px solid transparent;
    background: var(--tone-crimson);
    color: var(--tone-crimson-ink);
    font-size: var(--composer-control-font-size);
    font-weight: 600;
    font-family: inherit;
    letter-spacing: 0;
    cursor: pointer;
    padding: 0 13px;
    height: 100%;
    border-radius: 999px;
    box-shadow: var(--elev-rest);
    transition: var(--motion-tone);
    opacity: 1;
    display: flex;
    align-items: center;
    gap: 6px;  /* a sign and its word (Talk) sit alike before and after the feature's stylesheet */
}
.chat-send-inline svg { display: block; }
.chat-send-inline:not(:disabled):hover { background: var(--tone-crimson-hover); }
.chat-send-inline:not(:disabled):active { filter: var(--press); }
.chat-send-inline:disabled { box-shadow: none; }

/* Owner capsules (DESIGN "Composer owner controls"): Context and Effort, each a glass capsule of
   the pills' height (ring glyph · name · value) that opens inline into its strip. Siblings of the
   pills; the same vocabulary as Swarm. The open strip never takes a second line: beside the other
   controls when it fits, alone while they step aside, scrolling inside itself only when nothing
   else fits (composer_owner_controls.js sets data-fit, data-tight, data-label and
   --capsule-seg-pad from measurement, and holds --capsule-value-width and --capsule-body-width
   while a capsule opens or closes). */
.chat-capsule {
    --capsule-band-inset: var(--space-1);
    --capsule-seg-pad: var(--space-2);
    /* The strip's end gutter: a pill-shaped mark is concentric with the rounded end, so the band
       inset is its clearance on every side (Effort's square bracket needs more, below). */
    --capsule-end-gutter: var(--capsule-band-inset);
    position: relative;
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    box-sizing: border-box;
    height: var(--composer-control-height);
    max-width: 100%;
    min-width: 0;
    border: 1px solid transparent;
    border-radius: 999px;
    background: var(--surface-capsule);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    box-shadow: var(--elev-rest);
    color: var(--text-secondary);
    font-size: var(--composer-control-font-size);
    font-weight: 600;
    letter-spacing: 0;
    white-space: nowrap;
    transition: var(--motion-tone);
}
.chat-capsule:not([data-open="true"]):active { filter: var(--press); }
/* The head: ring, name, value. Closed it is the whole capsule; open, the value gives way to the
   strip and the name stays unless the strip needs its room. */
.chat-capsule-head {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    align-self: stretch;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.18s;
}
.chat-capsule-head:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}
@media (hover: hover) {
    .chat-capsule-head:hover {
        background: var(--accent-10);
    }
}
.chat-capsule-glyph {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: calc(var(--composer-control-height) - 2px);
    height: 100%;
}
.chat-capsule-glyph svg {
    display: block;
    width: 22px;
    height: 22px;
    overflow: visible;
}
@media (hover: hover) {
    .chat-capsule-head:hover { background: var(--surface-capsule-hover); }
}
.chat-capsule-name {
    margin: 0;
    margin-inline: -2px var(--space-2);
}
.chat-capsule-value {
    max-width: var(--capsule-value-width, none);
    margin: 0;
    margin-inline: -2px var(--space-3);
    overflow: hidden;
    color: var(--text-meta);
    /* The strip's curve and duration: the value gives way exactly as the strip grows, so the
       capsule never stands wider than closed or open (the fit measures only those). */
    transition: max-width 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), margin 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.2s;
}
.chat-capsule[data-open="true"] .chat-capsule-value {
    max-width: 0;
    margin: 0;
    opacity: 0;
}
.chat-capsule[data-open="true"][data-label="false"] .chat-capsule-name {
    display: none;
}
.chat-capsule .chat-capsule-name { font-weight: 500; }
.chat-capsule[data-known="false"] .chat-capsule-value { color: var(--text-secondary); }
.chat-capsule[data-known="false"] :is(.chat-effort-glyph-band, .chat-effort-glyph-dot) { visibility: hidden; }
.chat-subagents-mode[data-mode="off"]:not([data-open="true"]) :is(.chat-capsule-name, .chat-capsule-value) { color: var(--text-meta); }
.chat-effort-glyph-rail,
.chat-context-arc {
    stroke: rgba(var(--neutral-rgb), 0.16);
}
.chat-effort-glyph-band {
    stroke: var(--accent-55);
}
.chat-effort-glyph-dot {
    fill: var(--accent);
}
.chat-capsule-body {
    display: flex;
    align-items: center;
    align-self: stretch;
    max-width: 0;
    overflow: hidden;
    opacity: 0;
    transition: max-width 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.22s;
}
/* The divider between the name and the levels; without the name the ring delimits them. */
.chat-capsule-body::before {
    content: "";
    flex: 0 0 auto;
    width: 1px;
    height: 16px;
    margin-inline-end: var(--capsule-band-inset);
    background: rgba(var(--neutral-rgb), 0.16);
}
.chat-capsule[data-label="false"] .chat-capsule-body::before {
    display: none;
}
.chat-capsule[data-open="true"] .chat-capsule-body {
    max-width: var(--capsule-body-width, none);
    opacity: 1;
}
/* Under a finger the strip's values keep the touch minimum (ui.css .ui-hit-area): each of Context's and Subagents'
   values, and Effort's strip, reaches above and below the line, never sideways, since values abut. The body then
   clips only sideways where the engine can (else it hides them, as before), and a strip that scrolls clips them
   itself, so it has none. */
@media (pointer: coarse) {
    @supports (overflow: clip) {
        .chat-capsule-body { overflow-x: clip; overflow-y: visible; }
    }
    .chat-capsule .chat-seg.ui-hit-area::after { inset-inline: 0; }
    .chat-capsule[data-fit="overflow"] :is(.chat-seg, .chat-effort-strip).ui-hit-area::after { content: none; }
}
/* Alone, the strip appears and disappears at once, together with the controls it replaces; a
   capsule the other one closes collapses at once too (data-snap). */
.chat-capsule:is([data-fit="solo"], [data-fit="overflow"], [data-snap="true"]) :is(.chat-capsule-body, .chat-capsule-value) {
    transition: none;
}
/* A room's ☰ is one of the controls that step aside: the fit gives the open strip the whole line. */
.chat-toolbar-row[data-owner-solo] > .composer-menu,
.chat-toolbar-row[data-owner-solo] .chat-composer-chips,
.chat-toolbar-row[data-owner-solo="effort"] :is(.chat-context-mode, .chat-subagents-mode),
.chat-toolbar-row[data-owner-solo="context"] :is(.chat-effort-range, .chat-subagents-mode),
.chat-toolbar-row[data-owner-solo="subagents"] :is(.chat-context-mode, .chat-effort-range) {
    display: none;
}
/* Alone, a strip grows from its capsule and folds back into it (capsule_morph.js). While it moves, the capsule's
   glass is its own clipped layer, and the controls that step aside fade where they stood, out of the line's flow:
   only opacity and the clip move, so the line never wraps. */
.chat-capsule[data-morph] {
    z-index: 1;
    isolation: isolate;
    border-color: transparent;
    background: transparent;
    box-shadow: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    transition: none;
}
.chat-capsule-surface {
    position: absolute;
    z-index: -1;
    top: -1px;
    bottom: -1px;
    left: var(--morph-left, -1px);
    width: var(--morph-width, calc(100% + 2px));
    box-sizing: border-box;
    border: 1px solid transparent;
    border-radius: 999px;
    background: var(--surface-capsule);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    box-shadow: var(--elev-rest);
    pointer-events: none;
}
.chat-toolbar-row[data-owner-leaving] {
    position: relative;
}
.chat-toolbar-row[data-owner-solo][data-owner-leaving] > [data-capsule-leaving] {
    display: var(--leave-display, flex);
    position: absolute;
    z-index: 2;
    left: var(--leave-x);
    top: var(--leave-y);
    width: var(--leave-w);
    height: var(--leave-h);
    box-sizing: border-box;
    margin: 0;
    pointer-events: none;
}
/* A line too narrow for every name (a 360 px phone, a longer translation) keeps the closed capsules' rings and values:
   composer_owner_controls.js measures the settled row and lets the names give way one at a time, the longest first
   (U123), and where that still wraps it cuts the longest values to one measured width. An open capsule keeps its name. */
.chat-capsule[data-name-fit="off"]:not([data-open="true"]) .chat-capsule-name {
    display: none;
}
.chat-toolbar-row[data-owner-compact="values"] .chat-capsule:not([data-open="true"]) .chat-capsule-value {
    max-width: var(--capsule-value-cap);
    text-overflow: ellipsis;
}
/* While a drag runs, and when the layout itself changes (opening, a resize, a new padding),
   the marks land with the words at once, so a pill never trails behind the word it names. */
.chat-capsule:is([data-dragging="true"], [data-settling="true"]) :is(.chat-effort-band, .chat-effort-handle, .chat-effort-seg, .chat-context-thumb) {
    transition: none;
}
/* At var(--space-1) of padding and below, a full capsule would cut the corners of the word's box. */
.chat-capsule[data-tight="true"] :is(.chat-effort-rec, .chat-context-thumb) {
    border-radius: calc(var(--capsule-seg-pad) + var(--space-1));
}

/* Context: exactly three modes, the chosen one under a glass thumb, in its mode's colour (Max the
   accent, full context; Low the neutral ink, reduced: a level the owner chose is not a warning; Nano the
   project fuchsia). Subagents' strip is the same mode strip (--mode-ink, --mode-fill), below. */
.chat-context-mode {
    --mode-ink: var(--accent-light);
    --mode-fill: var(--accent-14);
}
.chat-context-mode[data-context-mode="low"] {
    --mode-ink: var(--status-neutral-fg);
    --mode-fill: var(--status-neutral-bg);
}
.chat-context-mode[data-context-mode="nano"] {
    --mode-ink: var(--project);
    --mode-fill: var(--project-18);
}
:is(.chat-context-mode, .chat-subagents-mode) .chat-seg[aria-checked="true"] {
    color: var(--mode-ink);
}
.chat-context-mode[data-context-mode="nano"] .chat-context-arc[data-arc="nano"],
.chat-context-mode[data-context-mode="low"] .chat-context-arc[data-arc="low"],
.chat-context-mode[data-context-mode="max"] .chat-context-arc[data-arc="max"] {
    stroke: var(--mode-ink);
}
.chat-context-strip {
    position: relative;
    flex: 0 0 auto;
    display: flex;
    align-self: stretch;
    margin-inline-end: var(--capsule-end-gutter);
}
:is(.chat-context-mode, .chat-subagents-mode)[data-disabled="true"] .chat-context-strip {
    opacity: 0.5;
    pointer-events: none;
}
.chat-context-thumb {
    position: absolute;
    top: var(--capsule-band-inset);
    bottom: var(--capsule-band-inset);
    left: 0;
    width: 0;
    border-radius: 999px;
    background: var(--mode-fill);
    box-shadow: inset 0 1px 0 rgba(var(--neutral-rgb), 0.06);
    pointer-events: none;
    transition: left 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.2s, background 0.2s;
}
:is(.chat-context-mode, .chat-subagents-mode) .chat-seg {
    position: relative;
    display: flex;
    align-items: center;
    padding: 0 var(--capsule-seg-pad);
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    letter-spacing: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color 0.15s;
}
.chat-seg-text {
    display: block;
}
:is(.chat-context-mode, .chat-subagents-mode) .chat-seg:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}
@media (hover: hover) {
    :is(.chat-context-mode, .chat-subagents-mode) .chat-seg[aria-checked="false"]:not([data-disabled="true"]):hover {
        color: var(--text-primary);
    }
}

/* Subagents: Off · Auto · Swarm on the same strip, the chosen one in its tone (Off the neutral ink, Auto and Swarm the
   accent); a value Settings rules out is disabled ink. The mark is a small network: Ouroboros in meta ink, its helpers
   and their threads in the rail's ink until the mode lights them: none, the first, every helper and the threads between
   them (Swarm). Closed, Off is muted and Auto and Swarm read like the neighbours' values: no fill, the network tells them
   apart (owner 28.10); Settings-off adds a small lock. Open, the hint above it says what the value means (ui_hint.js). */
.chat-subagents-mode {
    --mode-ink: var(--accent-light);
    --mode-fill: var(--accent-14);
}
.chat-subagents-mode[data-mode="off"] {
    --mode-ink: var(--status-neutral-fg);
    --mode-fill: var(--status-neutral-bg);
}
.chat-subagents-strip .chat-context-thumb { left: var(--thumb-left, 0); width: var(--thumb-width, 0); }
.chat-subagents-mode .chat-seg { justify-content: center; min-width: var(--touch-target-min); box-sizing: border-box; }
.chat-subagents-mode .chat-seg[data-disabled="true"] { color: var(--text-disabled); cursor: default; }
.chat-subagents-core { color: var(--text-meta); }
.chat-subagents-helper, .chat-subagents-link { color: rgba(var(--neutral-rgb), 0.28); }
.chat-subagents-mode[data-mode="auto"] .chat-subagents-helper[data-helper="1"],
.chat-subagents-mode[data-mode="swarm"] :is(.chat-subagents-helper, .chat-subagents-link) { color: var(--accent); }
.chat-subagents-mode[data-known="false"] :is(.chat-subagents-helper, .chat-subagents-link) { color: rgba(var(--neutral-rgb), 0.28); }
.chat-subagents-lock { display: none; align-items: center; flex: none; margin-inline-end: var(--space-3); color: var(--text-meta); }
.chat-subagents-mode[data-locked="true"] .chat-subagents-lock { display: inline-flex; }
.chat-subagents-mode[data-locked="true"]:not([data-open="true"]) .chat-capsule-value { margin-inline-end: 5px; }
.chat-subagents-mode[data-locked="true"][data-open="true"] .chat-subagents-lock { margin-inline-end: var(--space-2); }

/* Effort: one segment per level, as wide as its word plus the padding; the band and the three
   handles sit over it and share one vertical inset, so their edges meet exactly. The max bracket
   is square: at the last level its corners would graze the rounded end, so the end gutter adds
   what the curve takes back at the bracket's height (about a sixth of the control's height) and
   the bracket keeps the band inset from the shell, as the concentric pill does. */
.chat-effort-range {
    --effort-cap-width: var(--space-2);
    --capsule-end-gutter: calc(var(--capsule-band-inset) + var(--composer-control-height) / 6);
}
.chat-effort-strip {
    position: relative;
    flex: 0 0 auto;
    display: flex;
    align-self: stretch;
    margin-inline-end: var(--capsule-end-gutter);
    touch-action: none;
    cursor: pointer;
}
.chat-effort-band {
    position: absolute;
    top: var(--capsule-band-inset);
    bottom: var(--capsule-band-inset);
    border-radius: 4px;
    background: var(--accent-10);
    pointer-events: none;
    transition: left 0.15s, width 0.15s;
}
.chat-effort-seg {
    position: relative;
    z-index: 2;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 var(--capsule-seg-pad);
    color: var(--text-secondary);
    pointer-events: none;
    transition: color 0.15s;
}
.chat-effort-seg[data-in="true"] {
    color: var(--text-primary);
}
.chat-effort-seg[data-rec="true"] {
    color: var(--accent-light);
}
.chat-effort-seg-text {
    display: block;
}
/* Handles: the pill is the recommended level, the brackets are min and max. A tap or a key
   moves every handle with the same short transition, so a pushed bracket keeps pace. */
.chat-effort-handle {
    position: absolute;
    top: var(--capsule-band-inset);
    bottom: var(--capsule-band-inset);
    box-sizing: border-box;
    padding: 0;
    border: 0;
    background: none;
    cursor: grab;
    touch-action: none;
    -webkit-tap-highlight-color: transparent;
    z-index: 3;
    transition: left 0.15s, width 0.15s, box-shadow 0.15s;
}
.chat-effort-handle::before {
    content: "";
    position: absolute;
    inset: 0;
}
.chat-effort-handle:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}
.chat-effort-handle[data-dragging="true"] {
    cursor: grabbing;
    z-index: 4;
}
/* The pill covers its whole segment; the word sits centred inside it. */
.chat-effort-rec {
    z-index: 1;
    border: 1px solid var(--accent-55);
    border-radius: 999px;
    background: var(--accent-22);
}
.chat-effort-rec[data-dragging="true"] {
    box-shadow: 0 0 0 4px var(--accent-18);
}
.chat-effort-cap {
    width: var(--effort-cap-width);
    border: 2px solid var(--accent);
    background: transparent;
}
/* A finger-sized hit area that grows outward, never over the pill or the head. */
.chat-effort-min {
    border-right: 0;
    border-radius: 4px 0 0 4px;
}
.chat-effort-min::before {
    inset: -10px 0 -10px -12px;
}
.chat-effort-max {
    border-left: 0;
    border-radius: 0 4px 4px 0;
    transform: translateX(-100%);
}
.chat-effort-max::before {
    inset: -10px -12px -10px 0;
}
/* Right to left the scale runs leftwards: each bracket faces the range from the other side. */
.chat-effort-strip:dir(rtl) .chat-effort-min {
    border: 2px solid var(--accent);
    border-left: 0;
    border-radius: 0 4px 4px 0;
    transform: translateX(-100%);
}
.chat-effort-strip:dir(rtl) .chat-effort-min::before {
    inset: -10px -12px -10px 0;
}
.chat-effort-strip:dir(rtl) .chat-effort-max {
    border: 2px solid var(--accent);
    border-right: 0;
    border-radius: 4px 0 0 4px;
    transform: none;
}
.chat-effort-strip:dir(rtl) .chat-effort-max::before {
    inset: -10px 0 -10px -12px;
}
.chat-effort-cap[data-dragging="true"] {
    box-shadow: 0 0 0 4px var(--accent-22);
}
@media (hover: hover) {
    .chat-effort-rec:hover {
        box-shadow: 0 0 0 4px var(--accent-18);
    }
    .chat-effort-cap:hover {
        box-shadow: 0 0 0 3px var(--accent-18);
    }
}
/* Nothing fits even at the tightest padding (a 320 px phone, a pane dragged very narrow):
   the strip scrolls inside the capsule and fades on the side that hides levels. A swipe on
   a word scrolls, a handle still drags, the ring stays put. */
.chat-capsule[data-fit="overflow"] {
    flex: 1 1 auto;
}
.chat-capsule[data-fit="overflow"][data-open="true"] .chat-capsule-body {
    flex: 1 1 auto;
    min-width: 0;
    max-width: none;
}
.chat-capsule[data-fit="overflow"] :is(.chat-effort-strip, .chat-context-strip) {
    flex: 1 1 auto;
    min-width: 0;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    touch-action: pan-x;
    --capsule-fade-to: right;  /* toward the strip's end */
}
.chat-capsule[data-fit="overflow"] :is(.chat-effort-strip, .chat-context-strip):dir(rtl) {
    --capsule-fade-to: left;
}
.chat-capsule[data-fit="overflow"] :is(.chat-effort-strip, .chat-context-strip)::-webkit-scrollbar {
    display: none;
}
.chat-capsule[data-fit="overflow"] [data-scroll="start"] {
    -webkit-mask-image: linear-gradient(to var(--capsule-fade-to), black calc(100% - var(--space-3)), transparent);
    mask-image: linear-gradient(to var(--capsule-fade-to), black calc(100% - var(--space-3)), transparent);
}
.chat-capsule[data-fit="overflow"] [data-scroll="middle"] {
    -webkit-mask-image: linear-gradient(to var(--capsule-fade-to), transparent, black var(--space-3), black calc(100% - var(--space-3)), transparent);
    mask-image: linear-gradient(to var(--capsule-fade-to), transparent, black var(--space-3), black calc(100% - var(--space-3)), transparent);
}
.chat-capsule[data-fit="overflow"] [data-scroll="end"] {
    -webkit-mask-image: linear-gradient(to var(--capsule-fade-to), transparent, black var(--space-3));
    mask-image: linear-gradient(to var(--capsule-fade-to), transparent, black var(--space-3));
}
@media (prefers-reduced-motion: reduce) {
    .chat-toolbar-row .chat-capsule,
    .chat-toolbar-row .chat-capsule * {
        transition: none;
    }
}


/* Trays (DESIGN.md design bible; an object is ui.css .ui-card, which every card takes in its markup). Inside an object
   a group or row is a tray: the soft neutral fill with the inner radius and no shadow; a tray inside a tray drops its
   fill, so fills never stack and its rows part by space alone. Neither has an edge: the transparent 1px border only
   keeps each box's size. */
:is(.agent-family-card, .harness-account-row, .harness-login-card, .settings-effort-card,
    .settings-model-card, .settings-custom-secret-row, .settings-extension-section, .mcp-server-card) {
    border: 1px solid transparent; background: var(--surface-tray); border-radius: var(--radius-inner); box-shadow: none;
}
:is(.agent-family-card, .harness-login-card, .settings-effort-card, .settings-model-card,
    .settings-extension-section, .mcp-server-card) :is(.harness-account-row, .harness-login-card, .settings-effort-card,
    .settings-model-card, .settings-custom-secret-row) {
    background: transparent;
}


/* Every layer over the room lies over one veil, the theme's --scrim (ui.css; U020): the dialogs here, the Project
   panel's, the file dialog's and the document reader's, and the feed under a long-pressed row. */
.ui-modal-backdrop,
:where(.marketplace-modal-backdrop) {
    position: fixed;
    inset: 0;
    z-index: 90;
    display: flex;
    justify-content: center;
    overflow-y: auto;
    background: var(--scrim);
}

/* A dialog stands in the middle of the window, clear of the bars (U024); one taller than the window starts at its top
   and scrolls (the auto margins centre only while there is room). While a phone's keyboard is up the window keeps its
   height under it, so a dialog stands at the top instead, its foot above the keyboard (M12). */
.ui-modal,
:where(.marketplace-modal) {
    width: 100%;
    margin-block: auto;
    display: flex;
    flex-direction: column;
    border: 1px solid transparent;
    background: var(--ui-modal-bg);
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
body.keyboard-open :is(.ui-modal, .marketplace-modal) { margin-block: 0 auto; }

.section-title {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-secondary);
    margin: 16px 0 12px;
}


.form-section { margin-bottom: 24px; }

.form-section h3 {
    font-size: var(--type-section);
    line-height: var(--line-title);
    font-weight: 600;
    margin-bottom: 12px;
}

.form-row {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: 12px;
}

.form-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.form-field label {
    font-size: 12px;
    color: var(--text-secondary);
}


.local-toggle {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 12px;
    color: var(--text-secondary);
    cursor: pointer;
    padding-bottom: 8px;
    white-space: nowrap;
}
.local-toggle input[type="checkbox"] {
    accent-color: var(--accent);
    width: 14px;
    height: 14px;
    cursor: pointer;
}
.local-toggle[title] {
    transition: color 0.2s;
}

.form-field input[type="range"] {
    -webkit-appearance: none;
    height: 4px;
    background: rgba(var(--neutral-rgb), 0.12);
    border-radius: 2px;
    border: none;
    padding: 0;
}

/* Logs (U026-U035, U140): the tab body scrolls; its filters stay at its top on the room's colour (the stream lives
   at its newest end, the filters must stay in reach) while the list, one card whose events are trays, scrolls under
   them. A task's event is the same tray with its kind and its timeline. The system face reads the sentences;
   monospace keeps only the time and the raw record. */
/* The body's top inset rides on the bar, so the bar sticks flush at the scroller's edge (a sticky box keeps clear of
   its scroller's padding, which let rows show above it). */
#page-logs { padding-top: 0; }
.logs-filters {
    --control-h: var(--control-h-sm);
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: center;
    padding: var(--space-4) 0 var(--space-2);
    background: var(--bg-primary);
}

/* The filter chips wrap on a phone: under a finger they take the touch minimum as their own height. */
@media (pointer: coarse) {
    .logs-filters { --control-h: var(--touch-target-min); }
}

.logs-inline-clear {
    margin-left: auto;
}

.logs-card { display: grid; gap: var(--space-2); min-width: 0; }
.logs-empty { margin: 0; color: var(--text-secondary); font-size: var(--type-body); line-height: var(--line-body); }
.logs-empty[hidden] { display: none; }

#log-entries {
    display: grid;
    gap: var(--space-2);
    min-width: 0;
    font-size: var(--type-body);
    line-height: var(--line-body);
}

/* A log row is a tray inside the Logs object: the soft neutral fill, the inner radius, no outline. */
.log-entry {
    padding: 10px 12px;
    border-radius: var(--radius-inner);
    display: flex;
    flex-direction: column;
    gap: 6px;
    border: 1px solid transparent;
    background: var(--surface-tray);
    min-width: 0;
}

.log-main {
    display: flex;
    gap: 8px;
    align-items: baseline;
    flex-wrap: wrap;
}

.log-entry .log-ts {
    color: var(--text-muted);
    flex-shrink: 0;
    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
    font-size: var(--type-meta);
    font-variant-numeric: tabular-nums;
}

.log-entry .log-type {
    font-weight: 700;
    flex-shrink: 0;
    font-size: var(--type-meta);
}

/* A level chip lies flat; the default `info` level says nothing and is not drawn (U028). */
.log-entry .log-phase {
    padding: 1px 8px;
    border-radius: 999px;
    font-size: var(--type-meta);
    border: 1px solid transparent;
    color: var(--text-secondary);
    background: rgba(var(--neutral-rgb), 0.04);
}
.log-entry .log-phase.info { display: none; }

.log-entry .log-phase:is(.start, .calling, .context, .progress, .thought, .boot, .working, .thinking) {
    color: var(--accent-light);
    background: var(--accent-10);
}

.log-entry .log-headline {
    color: var(--text-primary);
    font-weight: 600;
    word-break: break-word;
}

.log-entry .log-repeat {
    margin-left: auto;
    color: var(--text-muted);
    font-size: var(--type-meta);
}

/* Raw is the line's last, quiet control (.btn.btn-ghost.btn-sm); after a count it follows it, else it takes the end. */
.log-raw-toggle { flex: none; align-self: center; }
.log-main > .log-repeat[hidden] + .log-raw-toggle { margin-left: auto; }

.log-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.log-pill {
    display: inline-flex;
    align-items: center;
    padding: 1px 8px;
    border-radius: 999px;
    background: rgba(var(--neutral-rgb), 0.05);
    color: var(--text-secondary);
    border: 1px solid transparent;
    font-size: var(--type-meta);
}

.log-body {
    color: var(--text-secondary);
    word-break: break-word;
    white-space: pre-wrap;
}

.log-raw {
    margin: 0;
    color: var(--text-secondary);
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

.log-type.tools { color: var(--status-info-fg); }
.log-type.llm { color: var(--accent); }
.log-type.errors { color: var(--status-error-fg); }
.log-type.tasks { color: var(--status-warn-fg); }
.log-type.system { color: var(--text-muted); }
.log-type.consciousness { color: var(--project); }

.log-task-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.log-task-review { margin-top: var(--space-2); }

/* Inside the task's tray its timeline rows part by space alone: a tray inside a tray drops its fill. */
.log-task-timeline {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.log-task-event {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* `width` is the vertical bar, `height` the horizontal one: without the height
   a horizontal bar falls back to the 16-17px UA default and paints thick. */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-corner { background: transparent; }
/* A scrollbar is the room's furniture: neutral, never the accent (DESIGN.md design bible). */
::-webkit-scrollbar-thumb { background: rgba(var(--neutral-rgb), 0.16); border-radius: var(--pill-radius); }
::-webkit-scrollbar-thumb:hover { background: rgba(var(--neutral-rgb), 0.28); }

.cost-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--type-body);
}
.cost-table th {
    text-align: left;
    font-weight: 500;
    color: var(--text-muted);
    font-size: var(--type-meta);
    padding: 4px 8px;
    border-bottom: 1px solid var(--divider);
}
.cost-table td {
    padding: 5px 8px;
    border-bottom: 1px solid rgba(var(--neutral-rgb), 0.04);
    color: var(--text-secondary);
}
.cost-table tbody tr:hover td {
    background: rgba(var(--neutral-rgb), 0.03);
}

/* Settings LAN reachability hint. */
/* The hint is a tray with its words in their own ink: a warning takes the hue's word, nothing takes an outline. */
.settings-lan-hint {
    margin-top: 8px;
    padding: 8px 12px;
    border-radius: var(--radius-inner);
    font-size: var(--type-meta);
    background: var(--surface-tray);
    border: 1px solid transparent;
    color: var(--text-secondary);
}
.settings-lan-hint[data-tone="warn"] { color: var(--status-warn-word, var(--text-secondary)); }
.settings-lan-hint a { color: inherit; text-decoration: underline; opacity: 0.9; }
/* Dashboard Evolution (evolution.js): the campaign object, then one time line of versions with the chosen version
   beneath it. Both are .ui-card objects on the shared materials. The chart's inks: the line and its area, milestone
   dots ringed in the object's own colour, ticks for every version and a dashed bridge over a break in the axis. */
:root {
    --evo-line: rgba(var(--neutral-rgb), 0.62);
    --evo-area: rgba(var(--neutral-rgb), 0.05);
    --evo-rug: rgba(var(--neutral-rgb), 0.26);
    --evo-rug-strong: rgba(var(--neutral-rgb), 0.5);
    --evo-bridge: rgba(var(--neutral-rgb), 0.3);
    --evo-ring: var(--surface-object-opaque);
}
.evo-meta { margin: 0; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }

/* Keep a reading measure for the campaign; status and actions reflow before they can squeeze its words. Its inset is
   the card's own (--card-pad). */
.evo-campaign { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-5); }
.evo-campaign-main, .evo-campaign-lines, .evo-campaign-side { display: grid; gap: 3px; min-width: 0; }
.evo-campaign-main { flex: 2 1 24rem; min-width: min(100%, 24rem); }
.evo-campaign-line { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.evo-campaign-chip { height: 20px; padding: 0 8px; font-size: var(--type-meta); font-weight: 500; }
.evo-campaign-side { flex: 1 1 17rem; }
.evo-campaign-side .ui-status { margin: 0; }
/* The pointer to the Consciousness tab is a quiet link with its state's dot (U071). Its state wraps on a narrow card
   ("sleeping until 4 Nov at 15:41"), so like the verbs it takes its box from its tokens and grows a line instead of
   spilling out of the lock's one-line box; one line stays the small capsule's 28 px. */
.evo-consciousness { justify-self: start; max-width: 100%; --control-h: auto; --control-pad-y: 5px; }
/* A campaign verb grows a line when a translation is long: the row's box comes from its tokens (the lock owns the
   property), one line staying the control's height. */
.evo-campaign-actions { --control-h: auto; --control-pad-y: var(--space-2);
    display: flex; flex-wrap: wrap; gap: var(--space-2); max-width: 100%; margin-inline-start: auto; }
.evo-campaign .evo-campaign-actions .btn { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
/* The verbs wrap onto rows of their own on a phone: under a finger each takes the touch minimum as its height (two
   lines of a long label still fit). */
@media (pointer: coarse) {
    .evo-campaign-actions { --control-h: var(--touch-target-min); --control-pad-y: 0px; }
}
.evo-campaign :is(.evo-meta, .ui-status) { overflow-wrap: anywhere; }
@container (max-width: 420px) {
    .evo-campaign-actions { margin-inline-start: 0; }
}

/* How Ouroboros changed: the head, one measure at a time (the shared segments, wrapping on a narrow card so none
   hides past its edge), the chart, how it is measured behind a disclosure, and the chosen version. */
.evo-story { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.evo-story-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3) var(--space-4); flex-wrap: wrap; }
.evo-story-words { display: grid; gap: 2px; min-width: 0; }
.evo-story-body { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.evo-story-status { display: flex; align-items: center; gap: 2px var(--space-2); flex-wrap: wrap; margin: 0; min-height: calc(var(--type-meta) * var(--line-meta)); }
.evo-story-status .ui-status { margin: 0; }
.evo-progress { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.evo-measures { --control-h: var(--control-h-sm); display: flex; flex-wrap: wrap; gap: 6px; }
/* The measures wrap on a narrow card: under a finger they take the touch minimum as their own height. */
@media (pointer: coarse) {
    .evo-measures { --control-h: var(--touch-target-min); }
}
.evo-measures-sep { width: 1px; align-self: stretch; margin: 0; background: rgba(var(--neutral-rgb), 0.12); }
.evo-quiet { --control-pad-x: var(--space-2); }
.evo-quiet svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.evo-link { padding: 0; border: 0; background: none; color: var(--text-primary); font-family: inherit; font-size: var(--type-meta);
    line-height: var(--line-meta); text-decoration: underline; text-decoration-color: rgba(var(--neutral-rgb), 0.3);
    text-underline-offset: 3px; cursor: pointer; }
.evo-link:hover { text-decoration-color: currentColor; }
:is(.evo-link, .evo-chart-hit):focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }

.evo-chart { position: relative; margin: 2px 0 0; }
.evo-chart svg { display: block; width: 100%; overflow: visible; }
.evo-chart text { font-family: var(--font-sans); font-size: var(--type-meta); fill: var(--text-meta); }
.evo-chart .evo-ch-strong { fill: var(--text-primary); font-weight: 600; }
.evo-chart .evo-ch-label { fill: var(--text-primary); font-weight: 600; paint-order: stroke; stroke: var(--evo-ring); stroke-width: 4px; stroke-linejoin: round; }
.evo-chart .evo-ch-mlabel { fill: var(--text-meta); font-weight: 500; paint-order: stroke; stroke: var(--evo-ring); stroke-width: 4px; stroke-linejoin: round; }
.evo-ch-grid { stroke: var(--chart-grid); stroke-width: 1; shape-rendering: crispEdges; }
.evo-ch-line { fill: none; stroke: var(--evo-line); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.evo-ch-area { fill: var(--evo-area); stroke: none; }
.evo-ch-bridge { fill: none; stroke: var(--evo-bridge); stroke-width: 1.5; stroke-dasharray: 2 4; stroke-linecap: round; }
.evo-ch-rug { stroke: var(--evo-rug); stroke-width: 1; shape-rendering: crispEdges; }
.evo-ch-rug.is-milestone { stroke: var(--evo-rug-strong); }
.evo-ch-pin { fill: var(--text-primary); stroke: var(--evo-ring); stroke-width: 2; }
.evo-ch-chosen { fill: var(--accent); stroke: var(--evo-ring); stroke-width: 2; }
.evo-ch-chosen.is-hollow { fill: var(--evo-ring); stroke: var(--accent); stroke-width: 1.5; }
.evo-ch-chosen-line { stroke: var(--accent); stroke-width: 1; opacity: 0.55; shape-rendering: crispEdges; }
.evo-ch-cross { stroke: rgba(var(--neutral-rgb), 0.35); stroke-width: 1; shape-rendering: crispEdges; }
.evo-ch-break { fill: none; stroke: var(--text-disabled); stroke-width: 1.5; }
.evo-chart-hit { position: absolute; inset: 0; cursor: crosshair; border-radius: var(--radius-inner); }
.evo-tip { position: absolute; z-index: 5; pointer-events: none; width: max-content; max-width: 280px; padding: 8px 12px;
    border-radius: var(--radius-inner); background: var(--surface-float); box-shadow: var(--elev-float);
    -webkit-backdrop-filter: var(--glass); backdrop-filter: var(--glass); }
.evo-tip b { display: block; color: var(--text-primary); font-size: var(--type-body); font-weight: 600; font-variant-numeric: tabular-nums; }
.evo-tip span { display: block; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.evo-tip .evo-tip-delta { display: inline; font-weight: 500; }
.evo-tip .evo-tip-head { color: var(--text-primary); margin-top: 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

.evo-detail { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; padding: 14px 16px 16px; border-radius: var(--radius-inner); background: var(--surface-tray); }
.evo-detail-top { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.evo-detail-top .evo-meta { flex: 1 1 auto; min-width: 0; }
.evo-detail-version { color: var(--text-primary); font-size: var(--type-meta); font-weight: 600; font-variant-numeric: tabular-nums; }
.evo-detail-nav { display: flex; gap: 2px; margin-left: auto; }
/* Under a finger the two arrows part by their sideways reaches. */
@media (pointer: coarse) {
    .evo-detail-nav { gap: var(--space-3); }
}
.evo-detail-title { margin: 0; color: var(--text-primary); font-size: var(--type-section); line-height: var(--line-title); font-weight: 600; overflow-wrap: anywhere; }
.evo-detail-lead { margin: 0; max-width: 68ch; color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); }
/* A tag annotation's body keeps its author's lines (its bullets); a README row has none. */
.evo-detail-body { margin: 0; max-width: 92ch; color: var(--text-meta); font-size: var(--type-meta); line-height: 1.5; overflow-wrap: anywhere;
    white-space: pre-line; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.evo-detail-body.is-full { display: block; -webkit-line-clamp: unset; overflow: visible; }
.evo-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); padding-top: 2px; }
.evo-fact { display: grid; gap: 1px; min-width: 0; }
.evo-fact .evo-meta { color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.evo-fact-value { color: var(--text-primary); font-size: var(--type-body); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.evo-fact-delta { margin-left: 6px; color: var(--text-meta); font-weight: 500; }
.evo-detail-source { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.evo-detail-source .evo-meta { overflow-wrap: anywhere; }
@container (max-width: 600px) {
    .evo-facts { grid-template-columns: minmax(0, 1fr); gap: 6px; }
    .evo-fact { grid-template-columns: 110px minmax(0, 1fr); align-items: baseline; }
    /* Beside its label a value may wrap before its change (the number and the change each stay whole), so a phone's
       tab never scrolls sideways for a "+7.1%". */
    .evo-fact-value { white-space: normal; }
    .evo-fact-delta { white-space: nowrap; }
}
.evo-empty { display: grid; gap: var(--space-2); justify-items: start; padding: 4px 0 2px; }
.evo-empty-line { margin: 0; max-width: 62ch; color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); }

/* Dashboard › Costs (costs.js) on the Dashboard tab body: the known spend of the limit leads with its share as a thin
   bar under it (amber only when the limit is reached, the owner's move); the budget form is the tab's action row; the
   four breakdowns are cards of their own, two side by side where they fit, each a short list with "Show all (N)". */
.costs-spend-bar { height: 4px; border-radius: var(--pill-radius); background: var(--status-neutral-bg); overflow: hidden; }
.costs-spend-bar > i { display: block; width: var(--spend-share, 0%); height: 100%; border-radius: inherit; background: var(--text-meta); }
.costs-spend-bar[data-reached="1"] > i { background: var(--amber); }
.costs-budget, .costs-table-card { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.costs-budget-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: var(--space-3) var(--space-5); align-items: start; }
.costs-copy { margin: 0; max-width: 72ch; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.costs-tables { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: var(--space-4); align-items: start; }
.cost-cell-name { width: 100%; max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cost-table .cost-cell-right { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.status-mark.cost-open { margin-inline-start: var(--space-1); vertical-align: middle; }
/* What the amount leaves out (pending, the calls without a price), in the meta ink under it. */
.cost-cell-note { display: block; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.cost-empty-cell { color: var(--text-muted); text-align: center; }
/* A narrow card reads a row as its name on its own line, then its calls and its amount (the header reads the same
   way): three columns there cut a name to its first letters and push the tab sideways. A name longer than its line
   still ends in an ellipsis in place. */
@container dashboard (max-width: 480px) {
    .cost-table, .cost-table :is(thead, tbody, th, td) { display: block; }
    .cost-table tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        column-gap: var(--space-3);
        border-bottom: 1px solid rgba(var(--neutral-rgb), 0.04);
    }
    .cost-table thead tr { border-bottom-color: var(--divider); }
    .cost-table :is(th, td) { border-bottom: 0; }
    .cost-table tr > :first-child { grid-column: 1 / -1; }
    .cost-table tr > :nth-child(2) { text-align: left; }
    .cost-table .cost-cell-name { width: auto; max-width: none; padding-bottom: 0; }
    .cost-table .cost-cell-note { white-space: normal; }
}

/* Dashboard Storage (the owner-started count and the free-space tray): shared card, type, spacing
   and tone tokens; only the bar's measured share arrives at runtime (--storage-share). */
/* The summary's words are the hero's own rows (.dashboard-hero), repainted as one region beside the kept actions. */
.storage-summary-text { display: contents; }
.dashboard-hero > .storage-summary-text > * { grid-column: 1; min-width: 0; margin: 0; }
/* The tab's one live region: screen readers hear its phase sentence; sighted owners read the summary. */
.storage-announce { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.storage-total.is-unknown,
.storage-place-size.is-unknown { color: var(--text-meta); }
.storage-kicker,
.storage-meta,
.storage-place-why,
.storage-place-split,
.storage-place-remover,
.storage-place-delta { color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.storage-meta { margin-top: var(--space-1); }
.storage-card-title {
    margin: 0;
    color: var(--text-primary);
    font-size: var(--type-section);
    line-height: var(--line-title);
    font-weight: 650;
}
.storage-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-2); }
/* A busy action keeps its focus (aria-disabled, not disabled) but stops looking pressable. */
:is(.storage-actions, .storage-summary-actions, .storage-free-picks) > .btn[aria-disabled="true"] { box-shadow: none; color: var(--text-meta); cursor: default; }
.storage-progress {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    margin: var(--space-1) 0 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.storage-progress > .status-mark { align-self: flex-start; margin-top: calc((var(--type-meta) * var(--line-meta) - var(--mark-fact)) / 2); }
/* The places a first count has already finished, under its progress line: nothing is summed yet. */
.storage-first-count { display: grid; gap: var(--space-1); margin: var(--space-2) 0 0; padding: 0; list-style: none; }
.storage-first-count > li { display: flex; justify-content: space-between; gap: var(--space-3); color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }

/* The bar: five group shares of a finished count, a 2 px surface gap between neighbours (a 1 px
   card-colour border is invisible in light), no track: a finished count fills it. */
.storage-bar { display: flex; width: 100%; min-height: 14px; gap: 2px; margin-top: var(--space-3); overflow: hidden; border-radius: var(--pill-radius); }
.storage-bar-seg { flex: 0 1 calc(var(--storage-share, 0) * 1%); min-width: 2px; background: var(--accent); }
.storage-key { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
:is(.storage-bar-seg, .storage-key)[data-group="records"] { background: var(--project); }
:is(.storage-bar-seg, .storage-key)[data-group="engine"] { background: var(--status-info-fg); }
:is(.storage-bar-seg, .storage-key)[data-group="work"] { background: var(--accent-light); }
:is(.storage-bar-seg, .storage-key)[data-group="temporary"] { background: var(--status-warn-fg); }
:is(.storage-bar-seg, .storage-key)[data-group="other"] { background: var(--text-secondary); }

/* The deleted projects' fold: a head row (name · size · growth) over its places. */
.storage-group { padding: var(--space-3) 0; border-top: 1px solid var(--divider); }
.storage-group:first-child { border-top: 0; }
.storage-group-head,
.storage-place {
    display: grid;
    grid-template-columns: 8px minmax(0, 1fr) auto minmax(56px, auto);
    column-gap: var(--space-2);
    row-gap: 2px;
    align-items: baseline;
}
.storage-group-head > .storage-key { grid-column: 1; grid-row: 1; align-self: center; }
.storage-group-label { grid-column: 2; min-width: 0; color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); font-weight: 650; overflow-wrap: anywhere; }
.storage-place-size { grid-column: 3; color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); text-align: end; white-space: nowrap; }
.storage-place-delta { grid-column: 4; text-align: end; white-space: nowrap; }
.storage-place-why,
.storage-place-split,
.storage-place-remover { grid-column: 2 / -1; }
.storage-place-why { overflow-wrap: anywhere; }  /* the server's reason can name an unbroken absolute path */
.storage-group-places { display: grid; margin: var(--space-2) 0 0 calc(8px + var(--space-2)); padding: 0; list-style: none; }
.storage-place.is-member { grid-template-columns: minmax(0, 1fr) auto minmax(56px, auto); padding-block: var(--space-2); border-top: 1px solid var(--divider); }
.storage-group-places > .storage-place:first-child { border-top: 0; }
/* A group folds under its head and one line; the caret is drawn because the row's own layout drops the native marker. */
.storage-group-fold > summary { position: relative; display: block; padding-inline-end: 20px; list-style: none; cursor: pointer; }
.storage-group-fold > summary::-webkit-details-marker { display: none; }
/* A caret drawn on a summary's ::after sets all four sides: under a finger ui.css gives every summary's ::after the
   touch reach's inset, and a side left unset would pull the caret onto the row's first column. */
.storage-group-fold > summary::after {
    content: '';
    position: absolute;
    inset-inline-end: 4px;
    inset-inline-start: auto;
    top: 7px;
    bottom: auto;
    width: 6px;
    height: 6px;
    border-right: 1.5px solid var(--text-meta);
    border-bottom: 1.5px solid var(--text-meta);
    transform: rotate(-45deg);
}
.storage-group-fold[open] > summary::after { transform: rotate(45deg); }
.storage-group-fold > summary:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; border-radius: var(--radius-inner); }
.storage-group-said { display: block; margin: 2px 0 0 calc(8px + var(--space-2)); color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.storage-group-open { margin-top: var(--space-2); }
.storage-group-open > .storage-group-places { margin-top: 0; }
.storage-place-label { grid-column: 1; min-width: 0; color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); font-weight: 500; overflow-wrap: anywhere; }
.storage-place.is-member > .storage-place-size { grid-column: 2; }
.storage-place.is-member > .storage-place-delta { grid-column: 3; }
.storage-place.is-member > :is(.storage-place-why, .storage-place-split, .storage-place-remover, .storage-parts) { grid-column: 1 / -1; }

/* Largest parts: the house disclosure gesture, listing a place's eight largest depth-1 parts. */
.storage-parts-list { display: grid; gap: var(--space-1); margin: var(--space-1) 0 0; padding: 0; list-style: none; }
.storage-parts-list > li { display: flex; justify-content: space-between; gap: var(--space-3); color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); overflow-wrap: anywhere; }
.storage-parts-rest { justify-content: flex-start; }

/* The fold of deleted projects' folders under the five groups. Everything else it uses is Storage's own.
   No key dot and a lighter name: it is not one of the bar's five shares. */
.storage-deleted-label { font-weight: 500; }
.storage-deleted-open { margin-left: calc(8px + var(--space-2)); }
.storage-deleted-open .storage-group-places { margin: 0; }
.storage-deleted-open > .storage-meta:first-child { margin-top: 0; }
.storage-deleted-never { margin-top: var(--space-3); }
.storage-deleted .storage-free-sub { margin-top: var(--space-4); }
.storage-deleted .storage-free-sub + .storage-meta { margin-top: 2px; }
.storage-deleted .storage-parts { margin-top: var(--space-1); border-top: 0; padding-top: 0; }
/* The same right edge as a fold's head, whose caret slot it has no use for. */
.storage-deleted-static { display: block; padding-inline-end: 20px; }
/* A row's opened parts keep their sizes in the row's size column. */
.storage-deleted .storage-parts-list { padding-inline-end: calc(56px + var(--space-2)); }
.storage-deleted .storage-place:focus { outline: none; }
.storage-deleted .storage-place:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; border-radius: var(--radius-inner); }

/* Free temporary space: the flow under the free-space reading in the first card. */
.storage-auto { margin: var(--space-3) 0 0; color: var(--text-secondary); font-size: var(--type-meta); line-height: var(--line-meta); }
.storage-tray { display: grid; gap: var(--space-2); margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--divider); }
.storage-tray:empty { display: none; }
.storage-tray p { margin: 0; }
.storage-tray-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-1) var(--space-3); }
.storage-tray-sum { color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); font-weight: 600; }
.storage-tray-text { color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); }
.storage-free-groups { display: grid; margin: 0; padding: 0; list-style: none; }
.storage-free-group {
    display: grid;
    grid-template-columns: 14px minmax(0, 1fr) auto;
    column-gap: var(--space-2);
    row-gap: 2px;
    align-items: baseline;
    padding-block: var(--space-2);
    border-top: 1px solid var(--divider);
}
/* The checkbox sits on the name's first line, also when the name wraps. */
.storage-free-group > .ui-checkbox { grid-column: 1; grid-row: 1; align-self: start; margin-top: calc((var(--type-body) * var(--line-body) - 14px) / 2); }
.storage-free-label {
    grid-column: 2;
    min-width: 0;
    color: var(--text-primary);
    font-size: var(--type-body);
    line-height: var(--line-body);
    font-weight: 600;
    overflow-wrap: anywhere;
    cursor: pointer;
}
.storage-free-size { grid-column: 3; color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); white-space: nowrap; }
.storage-free-why { grid-column: 2 / -1; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
/* Rows of a read-only list: no checkbox column, the name is plain text. */
.storage-free-groups.is-single > .storage-free-group { grid-template-columns: minmax(0, 1fr) auto; }
.storage-free-groups.is-single .storage-free-label { grid-column: 1; cursor: auto; }
.storage-free-groups.is-single .storage-free-size { grid-column: 2; }
.storage-free-groups.is-single .storage-free-why { grid-column: 1 / -1; }
.storage-tray .storage-largest { margin-top: 0; border-top: 0; padding-top: 0; }
.storage-largest > .storage-meta { margin-top: var(--space-2); }
.storage-largest-list { display: grid; gap: var(--space-2); margin: var(--space-2) 0 0; padding: 0; list-style: none; }
.storage-largest-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--space-3); }
.storage-largest-name { color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); overflow-wrap: anywhere; }
.storage-largest-size { color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); white-space: nowrap; }
.storage-largest-group { grid-column: 1 / -1; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.storage-tray > .storage-actions > .storage-meta { margin: 0; }
.storage-free-more { padding-top: 0; }
.storage-free-more .storage-free-groups { margin-top: var(--space-1); }
.storage-free-list > .storage-free-group:first-child { border-top: 0; }
.storage-free-path { overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
/* A copy offered by hand: what it would lose is the row's one line of ink, its largest unsaved paths fold under it. */
.storage-free-risk { color: var(--text-primary); }
.storage-free-group > .storage-free-paths { grid-column: 2 / -1; margin-top: var(--space-1); }
.storage-free-paths .storage-largest-list { gap: 2px; }
.storage-free-paths :is(.storage-largest-name.storage-free-path, .storage-largest-size) { font-size: var(--type-meta); line-height: var(--line-meta); }
.storage-free-paths .storage-largest-size { color: var(--text-meta); }
.storage-free-sub { margin: var(--space-3) 0 0; color: var(--text-primary); font-size: var(--type-meta); line-height: var(--line-meta); font-weight: 650; }
.storage-free-picks { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); margin-top: var(--space-2); }
.storage-free-picks > .storage-meta { margin: 0 auto 0 0; }

/* A size that carries a qualifier ("3.7 GB (some could not be read)") is too long to share the name's line on a
   narrow card: it goes under the name, so a group's name never shrinks to a column of a few letters. The tab body's
   width decides, not the viewport's. */
@container dashboard (max-width: 520px) {
    .storage-free-size.is-qualified { grid-column: 2; grid-row: 2; justify-self: start; white-space: normal; }
    .storage-free-groups.is-single .storage-free-size.is-qualified { grid-column: 1; }
    /* As in the free-space lists: on a narrow card a qualified size goes under the name. */
    .storage-place.is-member > .storage-place-size.is-qualified { grid-column: 1 / -1; grid-row: 2; justify-self: start; }
}

/* A phone stacks everything: full-width capsules, Measure first, Stop under it; size on the name's
   line, growth under it. */
@media (max-width: 640px) {
    .storage-actions { flex-direction: column; align-items: stretch; }
    .storage-actions > .btn { width: 100%; }
    .storage-group-head { grid-template-columns: 8px minmax(0, 1fr) auto; }
    .storage-group-head > .storage-place-delta { grid-column: 2 / -1; text-align: start; }
    .storage-place.is-member { grid-template-columns: minmax(0, 1fr) auto; }
    .storage-place.is-member > .storage-place-delta { grid-column: 1 / -1; text-align: start; }
    .storage-free-groups.is-single > .storage-free-group { grid-template-columns: minmax(0, 1fr) auto; }
    .storage-deleted .storage-parts-list { padding-inline-end: 0; }
}

/* Footprint → Cleanup → every folder (owner's selected composition, 4 November). */
.storage-legend { display:flex; flex-wrap:wrap; gap:2px var(--space-4); margin:var(--space-2) 0 0; padding:0; list-style:none; color:var(--text-meta); font-size:var(--type-meta); line-height:var(--line-meta); }
.storage-legend > li { display:inline-flex; align-items:center; gap:6px; }
.storage-legend b { color:var(--text-primary); font-weight:600; font-variant-numeric:tabular-nums; }
.storage-folders { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:var(--space-5); margin-top:var(--space-2); }
.storage-folders > ul { padding:0; margin:0; list-style:none; }
.storage-folder { border-top:1px solid var(--divider); }
.storage-folder-fold > summary { position:relative; display:grid; grid-template-columns:8px minmax(0,1fr) auto 64px; align-items:center; column-gap:var(--space-2); min-height:32px; padding:4px 18px 4px 0; list-style:none; cursor:pointer; }
.storage-folder-fold .storage-place-label { grid-column:2; }
.storage-folder-fold > summary::-webkit-details-marker { display:none; }
.storage-folder-fold > summary::after { content:''; position:absolute; inset:50% 4px auto auto; width:5px; height:5px; margin-top:-4px; border-right:1.5px solid var(--text-meta); border-bottom:1.5px solid var(--text-meta); transform:rotate(-45deg); }
.storage-folder-fold[open] > summary::after { transform:rotate(45deg); }
.storage-folder-fold > summary:focus-visible { outline:2px solid var(--focus-accent-border); outline-offset:2px; border-radius:var(--radius-inner); }
.storage-folder-open { display:grid; gap:2px; padding:0 0 var(--space-2) calc(8px + var(--space-2)); }
.storage-folder-open > * { grid-column:1 / -1; }
.storage-disk { margin-top:var(--space-3); }
.storage-cleanup .storage-tray { margin-top:var(--space-3); padding-top:0; border:0; }
.storage-cleanup-figures { display:grid; grid-template-columns:minmax(0,1.2fr) minmax(0,1fr) auto; align-items:center; gap:var(--space-2) var(--space-5); }
.storage-cleanup-figure { display:grid; gap:2px; min-width:0; }
.storage-cleanup-figure > .storage-meta { margin:0; }
.storage-cleanup-amount { color:var(--text-primary); font-size:var(--type-page); line-height:var(--line-title); font-weight:650; font-variant-numeric:tabular-nums; overflow-wrap:anywhere; }
.storage-cleanup-figure.is-quiet .storage-cleanup-amount { font-size:var(--type-section); }
.storage-cleanup-door { margin-top:var(--space-3); justify-content:flex-start; }
.storage-cleanup.has-door { display:grid; grid-template-columns:minmax(0,1fr) auto; column-gap:var(--space-4); }
.storage-cleanup.has-door > :is(.storage-card-title,.storage-auto) { grid-column:1 / -1; }
.storage-cleanup.has-door > .storage-cleanup-door { align-self:center; }
.storage-decisions { margin-top:var(--space-3); padding-top:var(--space-3); border-top:1px solid var(--divider); }
.storage-decisions > .storage-free-sub { margin-top:0; }
.storage-decision-rows { margin:var(--space-1) 0 0; padding:0; list-style:none; }
.storage-decision-row { display:grid; grid-template-columns:minmax(0,1fr) auto auto; align-items:center; gap:var(--space-1) var(--space-3); padding-block:var(--space-2); border-bottom:1px solid var(--divider); }
.storage-decision-label { display:grid; min-width:0; color:var(--text-primary); font-size:var(--type-body); line-height:var(--line-body); font-weight:500; overflow-wrap:anywhere; }
.storage-decision-label > .storage-meta { margin:0; font-weight:400; }
.storage-decision-size { color:var(--text-primary); font-size:var(--type-body); white-space:nowrap; font-variant-numeric:tabular-nums; }
@container dashboard (max-width:560px) {
 .storage-cleanup.has-door { display:block; }
 .storage-folders, .storage-cleanup-figures { grid-template-columns:minmax(0,1fr); }
 .storage-folder-fold > summary { min-height:44px; }
 .storage-decision-row { grid-template-columns:minmax(0,1fr) auto; }
 .storage-decision-size.is-qualified { grid-column:1 / -1; grid-row:2; justify-self:start; white-space:normal; }
 .storage-decision-row > .btn { grid-column:1 / -1; justify-self:start; }
}

#page-dashboard {
    min-height: 0;
}

.dashboard-shell {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: 0 20px 18px;
    overflow: hidden;
}

/* Dashboard tabs inherit shared .app-tab* styling. */

.dashboard-panels,
.dashboard-panel {
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

/* Dashboard panels own the scroll-chain for embedded sub-panels. */
.dashboard-panels {
    display: flex;
    flex-direction: column;
}

.dashboard-panel {
    display: none;
}

.dashboard-panel.active {
    display: flex;
    flex-direction: column;
}

/* === The Dashboard tab body (U140, U062, U065-gutter) ==================================================================
   Every Dashboard tab mounts ONE root, <div class="dashboard-body">, into its panel and places .ui-card objects in it.
   The body owns what used to differ per tab: the inset (16 px top, the page gutter at the sides), the stable scrollbar
   gutter (switching tabs never moves a card's edge), and the one gap between cards. A tab that opens with a headline
   number puts .dashboard-hero first in a .ui-card: kicker, figure, one meta line, and its actions beside them (under
   them when the body is narrow). Tab modules keep no scroller, inset or card padding of their own. */
.dashboard-body {
    flex: 1;
    min-height: 0;
    /* `scroll`, not `auto`: a WebKit without scrollbar-gutter keeps its classic bar's room on a tab that fits too (M14). */
    overflow-y: scroll;
    scrollbar-gutter: stable;
    padding: var(--space-4) 20px var(--space-5);
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    container: dashboard / inline-size;
}
.dashboard-hero {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--space-4);
    row-gap: var(--space-1);
    align-items: start;
}
.dashboard-hero > :not(.dashboard-hero-actions) { grid-column: 1; min-width: 0; margin: 0; }
.dashboard-hero-kicker,
.dashboard-hero-meta { color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.dashboard-hero-figure {
    color: var(--text-primary);
    font-size: var(--type-page);
    line-height: var(--line-title);
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}
.dashboard-hero-actions {
    grid-column: 2;
    grid-row: 1 / span 3;
    align-self: center;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}
@container dashboard (max-width: 480px) {
    .dashboard-hero-actions { grid-column: 1; grid-row: auto; align-self: start; }
}
/* A Dashboard card's title (16/600) and its list of facts (label, value): the tabs' cards share these two parts of the
   tab body (Costs, Consciousness, Updates, Activity, Evolution). */
.dashboard-card-title { margin: 0; color: var(--text-primary); font-size: var(--type-section); line-height: var(--line-title); font-weight: 600; }
.dashboard-facts {
    display: grid;
    grid-template-columns: fit-content(14rem) minmax(0, 1fr);
    gap: var(--space-1) var(--space-4);
    margin: 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.dashboard-facts dd { margin: 0; color: var(--text-primary); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
/* A narrow tab body reads each fact as its label over its value. */
@container dashboard (max-width: 480px) {
    .dashboard-facts { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
    .dashboard-facts dd { margin-bottom: var(--space-1); }
}
/* A headline card's Details follow the hero. Under a finger every control and summary reaches out to the touch minimum
   (ui.css "Touch reach"); a disclosure that follows the headline's actions (or the letter's last line) keeps the two
   reaches apart, so neither covers the other. */
.dashboard-hero + .ui-disclosure { margin-top: var(--space-3); }
@media (pointer: coarse) {
    .dashboard-hero + .ui-disclosure, .updates-letter-details { margin-top: var(--space-5); }
}

/* P4: Activity subtab — what runs now, the background mind, scheduled tasks: three cards on the Dashboard tab body.
   A row is the shared .ui-row tray (title clamped to two lines, one meta line, its controls beside the words or under
   them in a narrow row, ui.css); only what the shared parts do not say lives here. Every meta line wraps whole: a held
   Continue's cause (#1721) is bounded owner text the owner must read to act, and a schedule's state is read in full. */
.activity-section { display: grid; gap: var(--space-3); min-width: 0; }
.activity-section-content { min-width: 0; }
.activity-section-content > .activity-group { margin-top: var(--space-2); }
.activity-lines { display: grid; gap: 2px; }
/* A row's one Details (the whole order, a schedule's codes) takes the row's full width under its words. */
.activity-row > .activity-details { grid-column: 1 / -1; min-width: 0; }
/* Under a finger the row's small actions (each with its reach) and the Details summary under them part by more than
   that reach, so a tap just under Stop… or Delete never opens Details. */
@media (pointer: coarse) {
    .activity-row > .activity-details { margin-top: var(--space-3); }
}
.activity-order,
.activity-facts { margin: 0; overflow-wrap: anywhere; }
.activity-order { color: var(--text-secondary); font-size: var(--type-body); line-height: var(--line-body); }
.activity-facts { color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
/* The state's shared status mark leads its word (moving while it works, the pause bars when paused). */
.activity-mark { margin-inline-end: var(--space-1); vertical-align: middle; }

/* A known outcome's shared chip and its quieter second fact lead the meta line. */
.activity-phase {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    vertical-align: middle;
}

.activity-tag {
    font-size: var(--type-meta);
    color: var(--text-meta);
}

.activity-empty {
    margin: 0;
    font-size: var(--type-body);
    color: var(--text-secondary);
}

/* P2: main-screen Update pill — a pointer to Dashboard -> Updates. */
/* "An update is available" is a classification, not an alarm and not a
   success: it is a pointer to Dashboard -> Updates, the shared neutral capsule
   (.btn.btn-default.btn-sm, docs/DESIGN.md "Status and chips"). Uncommitted
   local changes ARE a caveat about what an update would do, the owner's turn
   in the amber tone; a failed attempt takes the red tone and an unknown one
   stays neutral; a confirmed success has no capsule (owner 1B). A tone outranks
   the shared hover, so a finger's lingering hover keeps it; a hovering pointer
   deepens it. */
.update-pill { margin: 6px 8px; }
.update-pill:is(.has-local, [data-tone="warn"]):not(:disabled) { background: var(--tone-amber); color: var(--status-warn-fg); }
.update-pill[data-tone="error"]:not(:disabled) { background: var(--tone-red); color: var(--status-error-fg); }
@media (hover: hover) {
    .update-pill:is(.has-local, [data-tone="warn"]):not(:disabled):hover { background: var(--tone-amber-hover); }
    .update-pill[data-tone="error"]:not(:disabled):hover { background: var(--tone-red-hover); }
}
/* The notice (update_status.js): the capsule and, for a failed or unconfirmed outcome, the close that ends it, drawn
   inside the capsule's end so its coming and going moves no neighbour. The close takes the capsule's ink. */
.update-notice { position: relative; display: inline-flex; align-items: center; min-width: 0; max-width: 100%; margin: 6px 4px 6px 8px; }
.update-notice[hidden] { display: none; }
.update-notice > .update-pill { min-width: 0; max-width: 100%; margin: 0; overflow: hidden; text-overflow: ellipsis; }
.update-notice > .update-pill.has-close { --control-pad-x: var(--space-3) 30px; }
.update-notice-close {
    position: absolute; right: 4px; top: 50%; width: 24px; height: 24px; margin-top: -12px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    border: 0; border-radius: 50%; background: transparent; color: var(--text-primary); cursor: pointer;
}
.update-notice-close[hidden] { display: none; }
.update-notice-close > svg { display: block; width: 14px; height: 14px; }
.update-pill[data-tone="error"] + .update-notice-close { color: var(--status-error-fg); }
.update-notice-close:active { filter: var(--press); }
/* Flush inside the capsule's end, so the ring stays in the capsule, clear of its edge and its last word (DESIGN §3). */
.update-notice-close:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: -2px; }
@media (hover: hover) {
    .update-notice-close:hover { background: rgba(var(--neutral-rgb), 0.16); }
}

.settings-embedded-content {
    display: flex;
    flex: 1;
    min-height: 0;
    flex-direction: column;
    overflow: hidden;
}

/* Skills page. */
#page-skills,
#page-widgets {
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
}

.skills-scroll,
.widgets-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: var(--space-4) var(--space-5) var(--space-5);
}

.skills-search-chrome {
    flex: 0 0 auto;
    margin: 2px 0 12px;
    padding: 0 var(--space-5);
    container: skillschrome / inline-size;
}

.skills-search-chrome[hidden] {
    display: none !important;
}

/* Activity's retained schedules (a fired one-shot, a withdrawn row) and Storage's folds; a Skills card's Details is
   the shared details.ui-disclosure (ui.css). */
.activity-history {
    margin-top: 4px;
    border-top: 1px solid rgba(var(--neutral-rgb), 0.06);
    padding-top: 10px;
}

.activity-history > summary {
    cursor: pointer;
    user-select: none;
    color: var(--text-meta);
    font-size: 12px;
    list-style: none;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.activity-history > summary::-webkit-details-marker {
    display: none;
}
/* Under a finger the fold's words are the touch minimum tall, like a shared disclosure's (ui.css "Touch reach"). */
@media (pointer: coarse) {
    .activity-history > summary { min-height: var(--touch-target-min); }
}

.activity-history > summary::before {
    content: '\25B8';
    display: inline-block;
    transition: transform 120ms ease;
    font-size: var(--type-meta);
}

.activity-history[open] > summary::before {
    transform: rotate(90deg);
}

.skills-card-menu {
    position: relative;
    display: inline-flex;
}

/* The card menu's ⋮ is one quiet icon control on Skills, Sources and Widgets cards: the shared small capsule without a
   body (.btn.btn-ghost.btn-sm, the quiet hover), a circle as tall as the row's controls, its glyph at the section size. */
.skills-card-menu-trigger { --control-pad-x: 0; --button-font-size: var(--type-section); width: var(--control-h); }

.skills-load-error {
    color: var(--status-error-fg);
    font-size: 12px;
    margin-bottom: 8px;
}

.toast-stack {
    --toast-top: calc(76px + env(safe-area-inset-top));
    position: fixed;
    right: 18px;
    top: var(--toast-top);
    z-index: 120;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
    pointer-events: none;
}

.toast {
    max-width: min(420px, calc(100vw - 112px));
    padding: 11px 14px;
    border-radius: var(--radius-object);
    border: 1px solid rgba(var(--neutral-rgb), 0.10);
    background: rgba(var(--modal-rgb), 0.94);
    color: var(--text-primary);
    box-shadow: 0 18px 42px rgba(0, 0, 0, 0.34);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    font-size: var(--type-body);
    line-height: 1.45;
    /* Error text can be a long unbroken token (a path, a URL, a serialized
       body); without this it overflows the toast instead of wrapping. */
    overflow-wrap: anywhere;
    pointer-events: auto;
    cursor: pointer;
    animation: toast-in 160ms ease-out;
}

.toast.is-hiding {
    animation: toast-out 160ms ease-in forwards;
}

/* A toast keeps its own glass background (a translucent status fill over the
   page would cost the text its contrast); the tone paints edge + foreground. */
.toast-ok { border-color: var(--status-ok-border); color: var(--status-ok-fg); }
.toast-danger { border-color: var(--status-error-border); color: var(--status-error-fg); }
.toast-warn { border-color: var(--status-warn-border); color: var(--status-warn-fg); }
.toast-muted,
.toast-info { color: var(--status-neutral-fg); }
/* Room notices sit above the measured composer, away from header menus. */
.toast-stack--room {
    position: absolute;
    inset: auto 14px var(--chat-input-reserve, 108px);
    align-items: center;
}
.toast-stack--room .toast { max-width: min(520px, 100%); box-sizing: border-box; background: var(--surface-float); }
/* A notice's one way back (host_modules.js recovery) follows its words on the same line when they fit. */
.toast .toast-action { display: inline-flex; flex-shrink: 0; margin-left: var(--space-2); vertical-align: middle; }
.toast:has(> .toast-dismiss) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-2); }
/* Each part asks for the close button among its own following siblings (toast.js appends words, action, close), so
   :has() stays in the subject compound. */
.toast > .toast-words:has(~ .toast-dismiss) { min-width: 0; grid-column: 1; }
.toast > .toast-action:has(~ .toast-dismiss) { grid-column: 1; margin-left: 0; justify-self: start; }
.toast > .toast-dismiss { grid-column: 2; grid-row: 1 / span 2; }
/* The page's stack ends 18 px above the screen's bottom, as it keeps from its right, and the notices it keeps scroll
   there as one column: every word and close button is reached by wheel, touch or keyboard (toast.js names it a region
   once cut). A scrolling box clips what it holds, so its padding is the toasts' shadow room (0 18px 42px): left and
   below them, and the 18 px to the right edge that `right` kept before. Room notices keep their own place. */
#toast-stack {
    right: 0;
    box-sizing: border-box;
    max-height: calc(var(--vvh) - var(--toast-top) - var(--inset-bottom) - 18px);
    padding: 0 18px 60px 42px;
    overflow-y: auto;
    overscroll-behavior: contain;
}
/* Cut (toast.js names it a region), the column takes the pointer itself, as any scrolling box does: a wheel or a finger
   anywhere over it scrolls it in every engine (Linux WebKit finds a scroller only where it takes pointer events).
   Uncut, its empty box lets the page through. */
#toast-stack[tabindex] { pointer-events: auto; }
/* A notice taller than the column shows keeps its close button on screen, at the nearer edge. */
#toast-stack .toast-dismiss { position: sticky; top: 0; bottom: 0; }
/* The focused scroll region uses the same ring as other keyboard controls. */
#toast-stack:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: -2px; }

@keyframes toast-in {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: translateY(0); }
}

@keyframes toast-out {
    from { opacity: 1; transform: translateY(0); }
    to { opacity: 0; transform: translateY(8px); }
}

/* A toast that stays glides into the place of one that left, while that one fades where it stood (toast.js writes
   only the measured shift). */
.toast { transition: transform var(--motion-close) var(--ease-sheet); }
.toast[data-gliding] { transform: translateY(var(--toast-shift, 0px)); transition: none; }
@media (prefers-reduced-motion: reduce) {
    .toast { transition: none; }
}

.skills-tab-panel {
    margin-top: 4px;
    container: skillspane / inline-size;
}

/* One card head for every skill (Installed, Browse, the detail sheet): the name opens the sheet, marks stay quieter
   than the name, the purpose reads in meta ink, the who line and facts are meta lines, Details follows the head. The
   switch is the run state; a chip beside it says only a state the switch cannot show; what needs the owner is an
   attention tray (one sentence and its actions), visible on touch.
   Work shows only as the three-dot loop. Values are ui.css tokens (docs/DESIGN.md "Airy dopamine", §8). */
.skills-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}
.skills-card {
    container: skillcard / inline-size;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}
/* Older WebViews isolate query containers; keep an open menu above siblings. */
.skills-card:has(.skills-card-menu-dialog[open]) { position: relative; z-index: 1; }
.skills-card[hidden] { display: none; }
.skills-card-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--space-4);
}
.skills-card-title { flex: 1 1 auto; min-width: 0; display: grid; gap: var(--space-1); }
.skills-card-toggle {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
    flex-wrap: wrap;
    justify-content: flex-end;
    min-width: 0;
}
.skills-head-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2); min-width: 0; }
/* A head that wraps (a long name, its marks on the next line) parts its lines under a finger by the two reaches between
   them (a name's and a mark's overshoot, together less than the touch minimum less --space-4), so neither covers the
   other; a head on one line is unchanged. */
@media (pointer: coarse) {
    .skills-head-title { row-gap: calc(var(--touch-target-min) - var(--space-4)); }
}
.skills-card-marks { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2); }
.skills-card-name {
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    text-align: left;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--type-section);
    font-weight: 600;
    line-height: var(--line-title);
    overflow-wrap: anywhere;
}
.skills-card-name:focus-visible,
.skills-read-more:focus-visible,
.skills-mark:focus-visible,
.skills-trust-value:focus-visible,
.skills-link-quiet:focus-visible,
.skills-link-button:focus-visible,
.skills-link:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}
.skills-card-name-more { margin-left: var(--space-1); color: var(--text-meta); font-weight: 400; }
.skills-mark {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 0;
    border: 0;
    background: none;
    color: var(--tone-crimson-word);
    font: inherit;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    font-weight: 500;
    white-space: nowrap;
}
button.skills-mark { cursor: pointer; min-height: 0; }
.skills-mark-glyph { display: inline-flex; }
.skills-mark svg { width: 14px; height: 14px; flex: 0 0 auto; }
.skills-card-purpose {
    margin: 0;
    color: var(--text-meta);
    font-size: var(--type-body);
    line-height: var(--line-body);
    overflow-wrap: anywhere;
}
.skills-card-purpose[data-clamped] { display: -webkit-box; -webkit-line-clamp: 8; -webkit-box-orient: vertical; overflow: hidden; }
.skills-read-more {
    align-self: flex-start;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--type-meta);
    font-weight: 600;
    cursor: pointer;
}
.skills-card-meta-lines { display: grid; gap: 2px; }
.skills-card-who,
.skills-card-facts,
.skills-card-note {
    margin: 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}
.skills-card-cats { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: 2px; }
.skills-link {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
    text-decoration-color: rgba(var(--neutral-rgb), 0.3);
}
.skills-link-quiet,
.skills-link-button {
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--type-meta);
    font-weight: 600;
}
.ui-chip.skills-chip { padding: 3px 10px; font-size: var(--type-meta); line-height: var(--line-meta); font-weight: 500; }
.ui-status.skills-status { display: inline-flex; align-items: center; min-width: 0; font-size: var(--type-meta); line-height: var(--line-meta); }
/* Live work's moving mark stands where the sentence's own dot would: one mark per place. */
.ui-status.skills-status[data-work]::before { display: none; }
.skills-status > .status-mark { margin-right: var(--space-1); }
.skills-status-text { min-width: 0; overflow-wrap: anywhere; }
.skills-skip-btn { display: inline-flex; align-items: center; gap: var(--space-1); }
.skills-skip-glyph { display: inline-flex; color: var(--status-warn-fg); }
.skills-skip-glyph svg { width: 14px; height: 14px; }

/* Attention trays: one sentence (and a quiet note) with its actions; a problem paints the error pair. */
.skills-card-trays { display: grid; gap: var(--space-2); }
.skills-card-trays:empty,
.skills-card [data-skill-errors]:empty { display: none; }
.skills-tray { padding: var(--space-3) var(--space-4); border-radius: var(--radius-inner); background: var(--surface-tray); }
.skills-tray[data-tone="danger"] { background: var(--status-error-bg); }
.skills-tray[data-tone="danger"] .skills-tray-text { color: var(--status-error-word, var(--text-primary)); }
.skills-tray-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); }
.skills-tray-text { min-width: 0; color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); overflow-wrap: anywhere; }
.skills-tray-note { margin-top: 2px; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.skills-tray-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
.skills-card-quiet-actions { display: flex; justify-content: flex-end; }
@container skillcard (max-width: 560px) {
    .skills-card-head { flex-direction: column; align-items: stretch; }
    .skills-card-toggle { justify-content: flex-start; }
    .skills-tray-row { grid-template-columns: minmax(0, 1fr); }
    .skills-tray-actions { justify-content: flex-start; }
}

/* The switch: green when on; a switch that cannot be used now loses its colour, never its size. */
.skills-switch {
    position: relative;
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    user-select: none;
    padding: 11px 4px;
    margin: -11px -4px;
}
.skills-switch input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.skills-switch-track {
    position: relative;
    display: inline-block;
    width: 40px;
    height: 22px;
    border-radius: var(--pill-radius);
    background: var(--status-neutral-border);
    transition: var(--motion-tone);
}
.skills-switch-thumb {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--surface-capsule);
    box-shadow: var(--elev-rest);
    transition: transform 140ms ease;
}
.skills-switch input:checked + .skills-switch-track { background: var(--green); }
.skills-switch input:checked + .skills-switch-track .skills-switch-thumb { transform: translateX(18px); }
.skills-switch input:focus-visible + .skills-switch-track { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
.skills-switch.is-locked,
.skills-switch input:disabled { cursor: not-allowed; }
.skills-switch.is-locked .skills-switch-track,
.skills-switch input:disabled + .skills-switch-track { background: var(--status-neutral-border); }
.skills-switch.is-locked .skills-switch-thumb,
.skills-switch input:disabled + .skills-switch-track .skills-switch-thumb { box-shadow: none; }

/* Details (the shared disclosure, right under the head, its rows the card's width): sentence-case labels in the meta
   ink at 500, values in the reading ink (DESIGN §2). */
.skills-details-body { display: grid; gap: var(--space-1); }
.skills-details-facts { display: contents; }
.skills-detail-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-2);
    align-items: baseline;
    padding: 2px 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.skills-detail-label { min-width: 72px; color: var(--text-meta); font-size: var(--type-meta); font-weight: 500; }
.skills-detail-value { min-width: 0; flex: 1 1 240px; overflow-wrap: anywhere; color: var(--text-primary); }
.skills-detail-row code { padding: 1px 6px; border-radius: var(--radius-inner); background: rgba(var(--neutral-rgb), 0.05); font-size: var(--type-meta); }
.skills-review-block { display: grid; gap: var(--space-1); color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.skills-review-block:empty { display: none; }
.skills-review-current { color: var(--text-meta); font-size: var(--type-meta); }
.skills-review-history ol,
.skills-review-findings ul,
.skills-card-warnings ul { margin: var(--space-1) 0 0; padding-left: 18px; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.skills-review-reason { color: var(--text-meta); font-size: var(--type-meta); overflow-wrap: anywhere; white-space: pre-wrap; }

/* Page chrome: the Installed search and quiet filters; Browse search, Source, Sort and Official only. */
.skills-search-chrome .skills-search { flex: 1 1 260px; min-width: min(200px, 100%); }
.skills-search-chrome[data-chrome-pane="installed"]:not([hidden]) { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.skills-filter-strip,
.skills-category-strip { display: flex; flex-wrap: wrap; gap: 2px var(--space-1); min-width: 0; }
/* On a phone the filters and the categories are one line that scrolls sideways ([data-scroll-x]: the drawer's swipe
   leaves it to the strip), never a lone chip wrapped onto a second row. Like a tab strip it fades on each side that
   still hides chips (scroll_fade.js sets the edges), and under a finger its chips take the touch minimum as their own
   height, since the strip clips a reach. */
html[data-layout="stack"] :is(.skills-filter-strip, .skills-category-strip) {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    --strip-fade-start: 0px;
    --strip-fade-end: 0px;
    -webkit-mask-image: linear-gradient(to right, transparent, black var(--strip-fade-start),
        black calc(100% - var(--strip-fade-end)), transparent);
    mask-image: linear-gradient(to right, transparent, black var(--strip-fade-start),
        black calc(100% - var(--strip-fade-end)), transparent);
}
html[data-layout="stack"] :is(.skills-filter-strip, .skills-category-strip)[data-scroll-start] { --strip-fade-start: var(--space-4); }
html[data-layout="stack"] :is(.skills-filter-strip, .skills-category-strip)[data-scroll-end] { --strip-fade-end: var(--space-4); }
html[data-layout="stack"] :is(.skills-filter-strip, .skills-category-strip)::-webkit-scrollbar { display: none; }
@media (pointer: coarse) {
    html[data-layout="stack"] :is(.skills-filter-strip, .skills-category-strip) { --control-h: var(--touch-target-min); }
}
.skills-filter-strip { flex-basis: 100%; margin-left: calc(-1 * var(--space-2)); }
.skills-category-strip { margin: 0 0 var(--space-3) calc(-1 * var(--space-2)); }
.skills-category-strip[hidden] { display: none; }
.filter-chip.skills-category {
    --control-pad-x: var(--space-2);
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    line-height: var(--line-title);
    color: var(--text-secondary);
    white-space: nowrap;
}
.skills-category-count { color: var(--text-meta); font-weight: 500; }
.filter-chip.skills-category.active .skills-category-count { color: inherit; }
.filter-chip.skills-category:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
.skills-browse-controls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); }
.skills-browse-filter { display: inline-flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }
.skills-browse-filter > span { color: var(--text-meta); font-size: var(--type-meta); }
.skills-browse-filter > select.ui-control { width: auto; min-width: 0; }
.skills-official-toggle { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--text-secondary); font-size: var(--type-body); cursor: pointer; }
.skills-list-status:empty { display: none; }
.skills-list-status { margin-bottom: var(--space-2); }
.skills-empty { margin: var(--space-3) 0; color: var(--text-meta); font-size: var(--type-body); line-height: var(--line-body); }
.skills-empty p { margin: 0 0 var(--space-2); }
@container skillschrome (max-width: 600px) {
    .skills-browse-filter { flex: 1 1 auto; }
    .skills-browse-filter > select.ui-control { flex: 1 1 auto; }
    .skills-search-chrome .skills-search { flex-basis: 100%; }
}

/* Browse: groups and the masonry board (masonry.js writes only custom properties; these rules apply them). */
.skills-browse { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.skills-browse-status { margin-bottom: var(--space-1); color: var(--text-meta); font-size: var(--type-meta); }
.skills-group-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-2) var(--space-3);
    margin: var(--space-5) 0 var(--space-3);
}
[data-browse-board] > .skills-group-head:first-child { margin-top: var(--space-1); }
.skills-group-title h3 { margin: 0; color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-title); font-weight: 600; }
.skills-group-count { color: var(--text-meta); font-weight: 400; }
.skills-group-title p { margin: 2px 0 0; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.skills-group-actions { display: flex; gap: var(--space-2); }
.skills-board { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 380px), 1fr)); gap: var(--space-3); align-items: start; }
.skills-board[data-masonry] { display: block; position: relative; height: var(--masonry-h, auto); }
.skills-board[data-masonry] > .skills-head-card {
    position: absolute;
    left: 0;
    top: 0;
    width: var(--masonry-w, auto);
    transform: translate(var(--masonry-x, 0px), var(--masonry-y, 0px));
}
.skills-head-card { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; overflow-wrap: anywhere; }
.skills-card-action-row { margin-top: auto; padding-top: var(--space-1); }
.skills-card-actions-end { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }

/* Sources: rows on the Accounts anatomy (DESIGN §6); the trust value is a setting, the health a status sentence. */
.skills-sources-card { gap: var(--space-3); }
.skills-sources-head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--space-3); }
.skills-sources-id { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
.skills-sources-id h3 { margin: 0; color: var(--text-primary); font-size: var(--type-section); line-height: var(--line-title); font-weight: 600; }
.skills-sources-copy { margin: 0; color: var(--text-meta); font-size: var(--type-body); line-height: var(--line-body); }
.skills-source-rows { display: grid; gap: var(--space-2); }
.skills-source-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "main actions" "meta meta";
    align-items: center;
    gap: var(--space-1) var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-inner);
    background: var(--surface-tray);
}
.skills-source-main { grid-area: main; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); min-width: 0; }
.skills-source-main strong { color: var(--text-primary); font-size: var(--type-body); font-weight: 600; }
.skills-source-actions { grid-area: actions; display: flex; align-items: center; gap: var(--space-2); }
.skills-source-meta { grid-area: meta; display: grid; gap: 2px; min-width: 0; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); overflow-wrap: anywhere; }
.skills-source-facts .ui-status { display: inline-flex; align-items: center; }
.skills-trust-value {
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    color: var(--text-meta);
    font: inherit;
    font-size: var(--type-body);
    font-weight: 600;
    text-align: right;
}
.skills-trust-value.is-suggested { color: var(--status-warn-fg); }
.skills-sources-foot p { margin: var(--space-2) 0 0; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
/* Under a finger the two lines' links part by their reaches (each line's link grows to the touch minimum). */
@media (pointer: coarse) {
    .skills-sources-foot p + p { margin-top: var(--space-5); }
}
@container skillspane (max-width: 600px) {
    .skills-source-row { grid-template-columns: minmax(0, 1fr); grid-template-areas: "main" "meta" "actions"; }
    .skills-source-actions { justify-content: space-between; }
    .skills-trust-value { text-align: left; }
}

/* Dialogs composed on the shared dialog chrome: offered-answer cards, the Add source steps, first start. */
.confirm-dialog.skills-dialog.is-wide { max-width: 640px; }
.skills-dialog .chat-quiz-option-detail { white-space: pre-line; }
.skills-dialog-note { margin: 0; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.skills-dialog-lead { margin: var(--space-2) 0 0; color: var(--text-primary); font-size: var(--type-body); font-weight: 600; }
.skills-dialog-error { margin: 0; }
.skills-add-field { display: grid; gap: var(--space-1); }
.skills-add-field > span { color: var(--text-meta); font-size: var(--type-meta); font-weight: 600; }
/* Add source: the optional revision and folder open under their own summary; a build's suggestions are offered cards. */
.skills-add-more > summary { width: fit-content; cursor: pointer; color: var(--text-primary); font-size: var(--type-body); font-weight: 600; }
.skills-add-more-fields { display: grid; gap: var(--space-2); margin-top: var(--space-2); }
.skills-suggestions .chat-quiz-option-detail { white-space: pre-line; overflow-wrap: anywhere; }
/* What a source could not offer: a warning count that opens to the exact reasons (Sources rows and Add source). */
.skills-source-problems { min-width: 0; }
.skills-source-problems > summary { width: fit-content; cursor: pointer; }
.skills-source-problems > summary .ui-status { display: inline-flex; align-items: center; }
.skills-source-problems ul {
    margin: var(--space-1) 0 0;
    padding-left: var(--space-4);
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}
.skills-first-rows { display: grid; gap: var(--space-2); container: skillsfirst / inline-size; }
.skills-first-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3);
    border-radius: var(--radius-inner);
    background: var(--surface-tray);
}
.skills-first-name { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2); min-width: 0; }
.skills-first-name strong { color: var(--text-primary); font-size: var(--type-body); font-weight: 600; }
.skills-first-about { flex-basis: 100%; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.skills-trust-segments { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
.skills-trust-segments .ui-segment { --button-font-size: var(--type-meta); }
.skills-mark-popup {
    max-width: min(360px, var(--ui-popup-max-width, 360px));
    padding: var(--space-3) var(--space-4);
    font-size: var(--type-body);
    line-height: var(--line-body);
}
.skills-mark-popup p { margin: 0 0 var(--space-2); }
@container skillsfirst (max-width: 520px) { .skills-first-row { grid-template-columns: minmax(0, 1fr); } }

/* The detail sheet: the delivered-document reader's geometry (a full sheet on a phone). */
.skills-detail-sheet .document-reader-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); white-space: normal; }
.skills-sheet-section { max-width: 720px; margin: 0 auto var(--space-5); }
.skills-sheet-section h3 { margin: 0 0 var(--space-1); color: var(--text-primary); font-size: var(--type-body); font-weight: 600; }
.skills-sheet-section p { margin: 0; color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); overflow-wrap: anywhere; }
.skills-sheet-section .skills-sheet-note,
.skills-sheet-status { margin: var(--space-1) auto 0; max-width: 720px; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.skills-sheet-status.is-error { display: grid; justify-items: start; gap: var(--space-2); color: var(--text-primary); }
.skills-sheet-about { color: var(--text-primary); font-size: var(--type-body); line-height: var(--line-body); }
.skills-sheet-details { max-width: 720px; margin: 0 auto; }
.skills-sheet-facts { display: grid; gap: var(--space-1); margin: var(--space-2) 0 0; }
.skills-sheet-facts div { display: grid; grid-template-columns: minmax(110px, 0.35fr) minmax(0, 1fr); gap: var(--space-2); }
.skills-sheet-facts dt { color: var(--text-primary); font-size: var(--type-meta); font-weight: 600; }
.skills-sheet-facts dd { margin: 0; color: var(--text-meta); font-size: var(--type-meta); overflow-wrap: anywhere; }


/* Detail modal. */

.marketplace-modal-backdrop {
    align-items: flex-start;
    padding: 4vh 16px;
}

.marketplace-modal {
    max-height: calc(var(--vvh) - 8vh);
    min-height: 0;
    border-color: transparent;
    border-radius: var(--radius-object);
    background: var(--popup-float);
    box-shadow: var(--elev-float);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    padding: 16px;
    max-width: 900px;
    gap: 12px;
}

/* Confirm/input/alert dialogs must stay usable over EVERY overlay (the Panic
   confirm is the canonical case: it must be clickable while the socket is
   down; no veil covers the app then, owner 20.10 Q4). Overlay scale:
   modals 90 · toasts/menus 120 · dialogs 1100. */
.confirm-dialog-backdrop {
    z-index: 1100;
}

.confirm-dialog {
    max-width: 520px;
}

.confirm-dialog .marketplace-modal-body p {
    margin: 0;
    white-space: pre-line;
}

/* A dialog carrying rendered prose (the update description before Apply) reads at a wider
   measure; the prose keeps its own bounded, keyboard-scrollable region so the consequences
   below it and the actions stay in view at every height. */
.confirm-dialog.has-content {
    max-width: 640px;
}

.confirm-dialog-content {
    max-height: min(42vh, 380px);
    overflow-y: auto;
    padding: 2px 12px 2px 0;
    border-bottom: 1px solid var(--divider);
}

.confirm-dialog-content:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

/* The dialog's own paragraphs are flush plain text; rendered prose keeps the rich-content rhythm. */
.confirm-dialog .confirm-dialog-content p {
    white-space: normal;
    margin-block: var(--space-2);
}
.confirm-dialog .confirm-dialog-content p:first-child { margin-block-start: 0; }
.confirm-dialog .confirm-dialog-content p:last-child { margin-block-end: 0; }
/* Beside rendered prose, the dialog's consequences and its Details summary read at the prose's
   size and ink, so the plain facts never outweigh what changes. Ordinary dialogs keep theirs. */
.confirm-dialog.has-content .marketplace-modal-body > p,
.confirm-dialog.has-content .confirm-dialog-details > summary {
    font-size: var(--type-body);
    line-height: var(--line-body);
    color: var(--text-primary);
}

/* The facts behind a dialog's one line open from the one disclosure (ui.css details.ui-disclosure). */
.confirm-dialog-detail-list {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

.confirm-dialog-detail-row {
    display: grid;
    grid-template-columns: minmax(110px, 0.35fr) minmax(0, 1fr);
    gap: var(--space-3);
}

.confirm-dialog-detail-row dt {
    font-weight: 600;
}

.confirm-dialog-detail-row dd {
    margin: 0;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}

.marketplace-modal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

/* A long unbroken title (a skill name) wraps; Close never leaves the dialog. */
.marketplace-modal-head > h3 { min-width: 0; overflow-wrap: anywhere; }
.marketplace-modal-head > button { flex: 0 0 auto; }

.marketplace-modal-head, .marketplace-modal-actions { flex-shrink: 0; }
.marketplace-modal-body > * { flex-shrink: 0; }
.marketplace-modal-body {
    min-height: 0;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    gap: 14px;
    overflow-y: auto;
}

.marketplace-modal-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 8px;
}

/* Widgets page. Masonry (web/modules/masonry.js) measures and packs the cards
   and writes the result ONLY as custom properties — `--masonry-h` on the list,
   `--masonry-w/-x/-y` on each card; these static rules apply them. Cards are
   positioned by transform in the owner's key order, so a reorder never moves a
   DOM node (a moved <iframe> would reload). The owner's column spans reach the
   masonry plan, not these rules (docs/DESIGN.md "Widgets board"). */

.widgets-list {
    position: relative;
    height: var(--masonry-h, auto);
}

.widgets-card {
    position: absolute;
    left: 0;
    top: 0;
    width: var(--masonry-w, auto);
    transform: translate(var(--masonry-x, 0px), var(--masonry-y, 0px));
}

.widgets-card.dragging {
    opacity: 0.68;
}

/* A widget being dropped on or resized wears the focus ring over its lift. */
.widgets-card.drag-over,
.widgets-card.resizing {
    box-shadow: var(--elev-float), 0 0 0 2px var(--focus-accent-border);
}

/* While a width is dragged: no text selection, and frames ignore the pointer so
   the drag keeps its events over them. */
.widgets-list.resizing {
    cursor: col-resize;
    user-select: none;
}

.widgets-list.resizing iframe {
    pointer-events: none;
}

/* The width handle: a thin strip on the card's right edge, the edge vocabulary
   of the side-panel `.resize-handle`. A list too narrow for two columns
   ignores widths, and a card no step can widen or narrow (the only card on the
   board) is width-fixed: neither offers the handle; the card menu sets widths
   everywhere. */
.widgets-card-resize {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 8px;
    padding: 0;
    border: 0;
    border-radius: 0 var(--radius-object) var(--radius-object) 0;
    background: transparent;
    cursor: col-resize;
    touch-action: none;
}

.widgets-card-resize:hover,
.widgets-card.resizing .widgets-card-resize {
    background: var(--accent-10);
}

.widgets-card-resize:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}

.widgets-list[data-widget-layout="stack"] .widgets-card-resize,
.widgets-card[data-widget-width-fixed] .widgets-card-resize {
    display: none;
}

/* The board's live region: width changes are announced, not shown; a failed
   save stays visible under the list (the shared status pair). */
.widgets-arrange-status:not([data-tone="error"]) {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

.widgets-arrange-status[data-tone="error"] {
    display: block;
    margin-top: 12px;
}

/* Each card is its own stacking context (transform + backdrop-filter), so an
   open card menu would otherwise paint under a later sibling card. */
.widgets-card:has(.skills-card-menu-dialog[open]) {
    z-index: 1;
}

/* The head: the title takes the room it needs and wraps; the menu and the move handle are one quiet pair at the head's
   end, in the same place on every card whatever the title, and a framed card's lifecycle controls drop under the pair
   (never the pair under them) when the card is too narrow for all of it. */
.widgets-card-head {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    align-items: flex-start;
    margin-bottom: 12px;
}

.widgets-card-title {
    display: flex;
    flex: 1 1 auto;
    flex-wrap: wrap;
    align-content: center;
    align-items: baseline;
    gap: 4px 8px;
    min-width: 0;
    min-height: 32px;
    /* A title breaks between its words: the card's own overflow-wrap (anywhere) split it inside one. */
    overflow-wrap: break-word;
    hyphens: auto;
}

/* The menu and the move handle are one quiet icon control twice: the ⋮ of every card menu (its rule is the Skills
   card menu's) and the ↕ share the shared small capsule without a body (.btn.btn-ghost.btn-sm), its touch reach
   included (DESIGN §3, bible principle 4). */
.widgets-card-drag { --control-pad-x: 0; --button-font-size: var(--type-section); width: var(--control-h); cursor: grab; }
.widgets-card-drag:active { cursor: grabbing; }

/* Framed-card head controls (widgets lifecycle phase 2): status as dot + text
   (docs/DESIGN.md section 4 — the status pair is the semantic contract, not a
   token migration of this surface), Start when stopped (Start again, neutral,
   after a mount that ended by itself), Stop in the card menu, and the drag handle. */
.widgets-card-controls {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 0 1 auto;
    flex-wrap: wrap-reverse;
    justify-content: flex-end;
    /* Never narrower than its widest part (the pair, or Start/Stop): a wrapping title takes the room, never the
       pair's place, so its ink never runs under the ⋮. */
    min-width: min-content;
}

/* The lifecycle controls (status, Start/Stop) wrap among themselves; the pair never does. */
.widgets-card-run {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 0 1 auto;
    flex-wrap: wrap;
    justify-content: flex-end;
    min-width: 0;
}

.widgets-card-run > :not(.ui-status) {
    flex: 0 0 auto;
}

/* A running card's Stop is the card menu's (rare, never the board's loudest word): the head offers Start only. The
   board's fork, a neutral Stop capsule in the head, is this one rule deleted (widget_card.js marks the button). */
.widgets-card-run [data-widget-power][data-running] { display: none; }

.widgets-card-tools {
    display: inline-flex;
    flex: 0 0 auto;
    align-items: center;
    gap: 4px;
}
/* Under a finger the pair parts by the two reaches (8px each side of a 28px capsule), so neither covers the other, and
   the head's lines (the pair over Start) part the same way when they wrap. */
@media (pointer: coarse) {
    .widgets-card-tools { gap: var(--space-4); }
    .widgets-card-controls { row-gap: var(--space-4); }
}

.widgets-card-controls .ui-status:not([hidden]) {
    display: inline-flex;
    align-items: center;
    min-width: 0;
    max-width: var(--widget-fault-status-max-width);
    white-space: normal;
    overflow-wrap: anywhere;
}

/* The sentence's own dot is ui.css's; the card's controls only tighten its spacing. */
.widgets-card-controls .ui-status[data-tone]::before {
    margin-right: 6px;
}

/* A narrow card (under 360px; widget_reorder.js marks it from the board's plan, a card being no query container of
   its own width) keeps only ⋮ and ↕ beside its title and puts its lifecycle row under it, so the title keeps the
   card's width and breaks between words. */
.widgets-card[data-widget-narrow] .widgets-card-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
.widgets-card[data-widget-narrow] .widgets-card-controls { display: contents; }
.widgets-card[data-widget-narrow] .widgets-card-title { grid-column: 1; grid-row: 1; }
.widgets-card[data-widget-narrow] .widgets-card-tools { grid-column: 2; grid-row: 1; align-self: start; }
.widgets-card[data-widget-narrow] .widgets-card-run { grid-column: 1 / -1; grid-row: 2; justify-content: flex-start; }
/* Under a finger the lifecycle row parts from ⋮ and ↕ above it by the two small capsules' reaches. */
@media (pointer: coarse) {
    .widgets-card[data-widget-narrow] .widgets-card-head { row-gap: var(--space-4); }
}

.widgets-menu-heading {
    padding: 4px 10px 6px;
    font-size: 12px;
    line-height: 1.35;
    color: var(--text-meta);
    white-space: nowrap;
}

.widgets-menu-item {
    display: flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
}

/* One check column, so a plain item (Reset size) lines up with the radios. */
.widgets-menu-check {
    flex: 0 0 auto;
    width: 1em;
}

.widgets-menu-item[aria-checked="false"] .widgets-menu-check {
    visibility: hidden;
}

.widgets-menu-note {
    max-width: 220px;
    margin: 0;
    padding: 0 10px 6px;
    font-size: 12px;
    line-height: 1.35;
    color: var(--text-meta);
}

/* A stopped framed card stands in for its frame: same box, same declared
   height (the `--widget-frame-height` custom property the frame itself uses),
   so Start does not move the layout. */
.widgets-facade {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    height: var(--widget-frame-height, 320px);
    min-height: 320px;
    border: 1px solid rgba(var(--neutral-rgb), 0.08);
    border-radius: var(--radius-inner);
    background: rgba(var(--shell-rgb), 0.88);
    color: var(--text-primary);
    text-align: center;
}

.widgets-facade-icon {
    display: inline-flex;
    font-size: 24px;
    line-height: 1;
    color: var(--text-meta);
}

.widgets-facade-icon svg {
    width: 1em;
    height: 1em;
}

.widgets-facade-title {
    font-size: 14px;
    line-height: 1.5;
    font-weight: 600;
}

.widgets-card-source {
    color: var(--text-meta);
    font-size: 12px;
}

.widgets-card-body > :first-child {
    margin-top: 0;
}

.widgets-frame {
    width: 100%;
    height: var(--widget-frame-height, 320px);
    min-height: 320px;
    display: block;
    border: 1px solid rgba(var(--neutral-rgb), 0.08);
    border-radius: var(--radius-inner);
    background: rgba(var(--shell-rgb), 0.88);
}

.widget-form,
.widget-group,
.widget-metric,
.widget-callout,
.widget-kv,
.widget-markdown,
.widget-json,
.widget-code,
.widget-chart,
.widget-tabs,
.widget-stream,
.widget-media,
.widget-gallery,
.widget-progress,
.widget-map,
.widget-calendar,
.widget-kanban,
.widget-subscription-render {
    margin-top: 10px;
}

/* Publisher markdown in a widget is DOMPurify-sanitized HTML, not the chat
   renderer's classed output, and the renderer stays unchanged (ARCHITECTURE,
   "Declarative widgets"). Its block elements therefore need element-level
   rhythm inside this one scope; paragraphs come from .ui-rich-content, which
   the widget host already applies alongside .widget-markdown. */
.widget-markdown h1,
.widget-markdown h2,
.widget-markdown h3 {
    margin: var(--space-4) 0 var(--space-2);
    color: var(--text-primary);
    line-height: var(--line-title);
}
.widget-markdown h1 { font-size: var(--type-section); }
.widget-markdown h2,
.widget-markdown h3 { font-size: var(--type-body); }
.widget-markdown blockquote {
    margin: var(--space-3) 0;
    padding: var(--space-2) var(--space-3);
    border-left: 3px solid var(--accent);
    background: var(--surface-tray);
    color: var(--text-meta);
}
.widget-markdown table {
    width: 100%;
    margin: var(--space-4) 0;
    border-collapse: collapse;
    color: var(--text-primary);
    font-size: var(--type-body);
}
.widget-markdown th,
.widget-markdown td {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--divider);
    color: var(--text-primary);
    text-align: left;
}
.widget-markdown th { font-weight: 600; }

.widget-json {
    min-width: 0;
    max-width: 100%;
}

.widget-json pre {
    max-width: 100%;
    max-height: min(360px, 50vh);
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.widget-form {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
/* An action (or a poll's button) under a form or under another action parts from it under a finger by the two
   buttons' reaches. */
@media (pointer: coarse) {
    :is(.widget-action, [data-widget-poll]) { margin-top: var(--space-3); }
}

.widget-form-fields {
    display: grid;
    gap: 10px;
    min-width: 0;
}

.widget-form-fields.widget-grid-cols-1 { grid-template-columns: minmax(0, 1fr); }
.widget-form-fields.widget-grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.widget-form-fields.widget-grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.widget-form-fields.widget-grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

.widget-field-span-2 { grid-column: span 2; }
.widget-field-span-3 { grid-column: span 3; }
.widget-field-span-4 { grid-column: span 4; }

.widget-field {
    min-width: 0;
}

.widget-field-inline {
    flex-direction: row;
    align-items: center;
}


.widget-group {
    border: 1px solid rgba(var(--neutral-rgb), 0.07);
    border-radius: var(--radius-inner);
    padding: 10px;
}

.widget-group > h4,
.widget-group > p { margin: 0 0 8px; }
.widget-group > p { color: var(--text-secondary); font-size: 12px; }
.widget-group-components { min-width: 0; }
.widget-group-components > * { margin-top: 0; }
.widget-group-stack > .widget-group-components { display: flex; flex-direction: column; gap: 10px; }
.widget-group-cluster > .widget-group-components { display: flex; flex-wrap: wrap; align-items: start; gap: 10px; }
.widget-group-grid > .widget-group-components { display: grid; gap: 10px; }
.widget-group-grid.widget-grid-cols-1 > .widget-group-components { grid-template-columns: minmax(0, 1fr); }
.widget-group-grid.widget-grid-cols-2 > .widget-group-components { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.widget-group-grid.widget-grid-cols-3 > .widget-group-components { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.widget-group-grid.widget-grid-cols-4 > .widget-group-components { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.widget-group-grid > .widget-group-components > :is(
    .widget-group,
    .widget-form,
    .widget-table-wrap,
    .widget-chart,
    .widget-tabs,
    .widget-kanban,
    .widget-code,
    .widget-json,
    .widget-subscription-render
) {
    grid-column: 1 / -1;
}

/* A metric or callout is a tray; its one rail at the start takes the tone's hue below, the rest has no outline. */
.widget-metric,
.widget-callout {
    border: 1px solid transparent;
    border-left-width: 3px;
    border-radius: var(--radius-inner);
    background: var(--surface-tray);
    padding: 9px 10px;
}

.widget-metric { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.widget-metric > span { color: var(--text-secondary); font-size: 12px; }
.widget-metric > strong { font-size: 18px; }
.widget-metric small { color: var(--text-secondary); font-size: var(--type-meta); font-weight: 500; }
.widget-callout { font-size: var(--type-body); line-height: 1.45; background: var(--surface-tray); }
.widget-metric[data-tone="ok"], .widget-callout[data-tone="ok"] { border-left-color: var(--status-ok-fg); }
.widget-metric[data-tone="warn"], .widget-callout[data-tone="warn"] { border-left-color: var(--status-warn-fg); }
.widget-metric[data-tone="danger"], .widget-callout[data-tone="danger"] { border-left-color: var(--status-error-fg); }
/* `info` is a widget-only tone with no status role; it stays the plain blue. */
.widget-metric[data-tone="info"], .widget-callout[data-tone="info"] { border-left-color: var(--blue); }

.widget-kv-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 6px 0;
    border-bottom: 1px solid rgba(var(--neutral-rgb), 0.06);
}

.widget-table-wrap {
    overflow-x: auto;
}

.widget-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--type-body);
}

.widget-table th,
.widget-table td {
    padding: 7px 8px;
    border-bottom: 1px solid rgba(var(--neutral-rgb), 0.06);
    text-align: left;
    overflow-wrap: break-word;
    word-break: normal;
}

.widget-table th,
.widget-table-status { white-space: nowrap; }

.widget-table a { color: var(--status-info-fg); overflow-wrap: anywhere; }
.widget-table-status { font-weight: 600; }
.widget-table-status[data-tone="ok"] { color: var(--status-ok-fg); }
.widget-table-status[data-tone="warn"] { color: var(--status-warn-fg); }
.widget-table-status[data-tone="danger"] { color: var(--status-error-fg); }
.widget-table-status[data-tone="neutral"],
.widget-table-status[data-tone="muted"] { color: var(--status-neutral-fg); }

.widget-media img,
.widget-media video {
    max-width: 100%;
    border-radius: var(--radius-inner);
}

.widget-media audio {
    width: 100%;
}

.widget-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 10px;
}

/* Host-owned map/calendar/kanban defaults without extra dependencies. */
.widget-map {
    border: 1px solid rgba(var(--neutral-rgb), 0.06);
    border-radius: var(--radius-inner);
    padding: 10px;
    background: rgba(var(--neutral-rgb), 0.02);
}
.widget-map-list { margin: 0; padding-left: 18px; font-size: var(--type-body); }
.widget-map-list li { padding: 2px 0; }

.widget-calendar { font-size: var(--type-body); }
.widget-calendar-list { list-style: none; margin: 0; padding: 0; }
.widget-calendar-row {
    display: flex;
    gap: 8px;
    align-items: baseline;
    padding: 6px 8px;
    border-bottom: 1px solid rgba(var(--neutral-rgb), 0.05);
}

.widget-kanban {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
    gap: 10px;
    margin-top: 8px;
}
.widget-kanban-col {
    align-self: start;
    background: rgba(var(--neutral-rgb), 0.03);
    border-radius: var(--radius-inner);
    padding: 10px;
    min-height: 120px;
}
.widget-kanban-col.is-empty { min-height: 72px; }
.widget-kanban-col-head { margin-bottom: 8px; font-size: 12px; color: var(--text-secondary); }
.widget-kanban-empty { color: var(--text-muted); font-size: var(--type-meta); }
.widget-kanban-card {
    display: flex;
    flex-direction: column;
    gap: 7px;
    background: var(--accent-12);
    border: 1px solid var(--accent-25);
    border-radius: 6px;
    padding: 6px 8px;
    margin: 4px 0;
    cursor: grab;
    font-size: 12px;
}
.widget-kanban-card:active { cursor: grabbing; }

.widget-kanban-move {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
}

.widget-kanban-move select { min-width: 0; max-width: 112px; }

.widget-progress {
    display: flex;
    align-items: center;
    gap: 10px;
}

.widget-code pre {
    margin: 0;
    padding: 10px;
    border-radius: var(--radius-inner);
    background: rgba(0, 0, 0, 0.24);
    overflow-x: auto;
}

.widget-code-label {
    color: var(--text-meta);
    font-size: 12px;
    margin-bottom: 6px;
}

.widget-chart {
    min-height: 240px;
}

/* Bounded canvas (v6.71.0): a fixed-height positioned box for Chart.js
   responsive sizing — charts stop growing the card unbounded, and the data
   table below stays reachable. */
.widget-chart-canvas { position: relative; height: clamp(260px, 32vh, 360px); }
.widget-chart-canvas canvas { display: block; width: 100%; }
.widget-chart-fallback { min-height: 0; }
.widget-chart-data { margin-top: 8px; }
.widget-chart-data > summary { color: var(--text-secondary); cursor: pointer; font-size: 12px; }

/* SWR refresh (v6.71.0): content stays; a thin pulsing indicator marks the
   background refetch instead of a loading swap. */
.widget-status[data-state="refreshing"] {
    position: relative;
}
.widget-status[data-state="refreshing"]::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -2px;
    height: 2px;
    border-radius: 1px;
    background: var(--accent);
    opacity: 0.5;
    animation: widget-refresh-pulse 1.2s ease-in-out infinite;
}
@keyframes widget-refresh-pulse {
    0%, 100% { opacity: 0.2; }
    50% { opacity: 0.7; }
}
@media (prefers-reduced-motion: reduce) {
    .widget-status[data-state="refreshing"]::after { animation: none; }
}

.widget-tab-list {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 10px;
}

/* A declarative widget's tabs are the shared quiet small capsule (.btn.btn-ghost.btn-sm); the chosen one keeps the
   accent's wash under the reading ink. */
.widget-tab-btn { color: var(--text-secondary); }

.widget-tab-btn.active {
    background: var(--accent-12);
    color: var(--text-primary);
}

.widget-tab-body > :first-child {
    margin-top: 0;
}

/* Late mobile overrides; placed after base rules so they win without !important. */
@media (max-width: 640px) {
    .widget-kanban-col.is-empty {
        min-height: 0;
        padding-block: 8px;
    }

    .widget-kanban-col.is-empty .widget-kanban-col-head {
        margin-bottom: 3px;
    }

    .widget-form-fields.widget-grid-cols-1,
    .widget-form-fields.widget-grid-cols-2,
    .widget-form-fields.widget-grid-cols-3,
    .widget-form-fields.widget-grid-cols-4,
    .widget-group-grid.widget-grid-cols-1 > .widget-group-components,
    .widget-group-grid.widget-grid-cols-2 > .widget-group-components,
    .widget-group-grid.widget-grid-cols-3 > .widget-group-components,
    .widget-group-grid.widget-grid-cols-4 > .widget-group-components {
        grid-template-columns: minmax(0, 1fr);
    }

    .widget-field-span-2,
    .widget-field-span-3,
    .widget-field-span-4 { grid-column: span 1; }

    .widget-table-wrap { overflow-x: visible; }
    .widget-table,
    .widget-table tbody,
    .widget-table tr,
    .widget-table td { display: block; width: 100%; }
    .widget-table thead { display: none; }
    .widget-table tr {
        border: 1px solid rgba(var(--neutral-rgb), 0.07);
        border-radius: var(--radius-inner);
        margin-bottom: 8px;
        padding: 5px 8px;
    }
    .widget-table td {
        display: grid;
        grid-template-columns: minmax(72px, 38%) minmax(0, 1fr);
        gap: 8px;
        border-bottom: 1px solid rgba(var(--neutral-rgb), 0.04);
        padding: 6px 0;
    }
    .widget-table td::before {
        content: attr(data-label);
        color: var(--text-secondary);
        font-size: var(--type-meta);
    }
    .widget-table th[scope="row"] { display: block; padding: 6px 0; }

    .dashboard-body { padding: var(--space-3); }
    .logs-filters { padding-top: var(--space-3); }

}

/* The stack layout's composer (Main; the Project panel keeps its own rules below) and the keyboard-in-flow layout
   app.js enters only there (`keyboard-open`, from the same switch: layout.js). */
html[data-layout="stack"] #chat-input-area {
    /* iOS safe-area inset. */
    padding-bottom: max(16px, var(--inset-bottom));
}

html[data-layout="stack"] .chat-input-wrap,
html[data-layout="stack"] .chat-field,
html[data-layout="stack"] .chat-text-row,
html[data-layout="stack"] .chat-toolbar-row,
html[data-layout="stack"] #chat-input,
html[data-layout="stack"] textarea.chat-input {
    min-width: 0;
}

html[data-layout="stack"] .chat-toolbar-row {
    --composer-row-gap: 6px;
    position: static;
    right: auto;
    bottom: auto;
    z-index: auto;
    justify-content: flex-start;
    padding: 0 6px;
}

/* While the owner reads back through the history the row of capsules folds away (composer_fold.js; owner 19.10, 2A):
   its line collapses from its measured height (--fold-row-h) with the wrap's gap, the capsules fade, ☰ stays the
   row's first control and lands beside the field on its last line, beside "+" (--fold-menu-dy), and the field makes
   room for it on its leading side, where ☰ leads the row in either direction (☰ and the row's 6px padding, then the
   row's gap).
   data-fold holds from the fold until the row is back, so all of it runs back on the same curve. */
html[data-layout="stack"] .chat-input-wrap[data-fold] .chat-toolbar-row {
    max-height: var(--fold-row-h);
    transition: max-height 300ms var(--ease-sheet), margin-bottom 300ms var(--ease-sheet);
}
html[data-layout="stack"] .chat-input-wrap[data-folded] .chat-toolbar-row {
    max-height: 0;
    margin-bottom: calc(-1 * var(--space-2));
}
html[data-layout="stack"] .chat-input-wrap[data-fold] .chat-toolbar-row > .composer-menu {
    transition: transform 300ms var(--ease-sheet);
}
html[data-layout="stack"] .chat-input-wrap[data-folded] .chat-toolbar-row > .composer-menu {
    transform: translateY(var(--fold-menu-dy, 0px));
}
html[data-layout="stack"] .chat-input-wrap[data-folded] .chat-toolbar-row > :not(.composer-menu) {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 120ms ease, visibility 0s linear 120ms;
}
/* Coming back, the capsules show once their line has opened enough to hold them. */
html[data-layout="stack"] .chat-input-wrap[data-fold]:not([data-folded]) .chat-toolbar-row > :not(.composer-menu) {
    transition: opacity 160ms ease 140ms, visibility 0s;
}
html[data-layout="stack"] .chat-input-wrap[data-fold] .chat-field {
    transition: box-shadow 0.2s, background 0.2s, margin-inline-start 300ms var(--ease-sheet);
}
html[data-layout="stack"] .chat-input-wrap[data-folded] .chat-field {
    margin-inline-start: calc(var(--composer-control-height) + 6px + var(--space-2));
}
/* Each as specific as the motion it stops: the line and the field, ☰, the capsules going and coming back. */
@media (prefers-reduced-motion: reduce) {
    html[data-layout="stack"] .chat-input-wrap[data-fold] :is(.chat-toolbar-row, .chat-field),
    html[data-layout="stack"] .chat-input-wrap[data-fold] .chat-toolbar-row > .composer-menu,
    html[data-layout="stack"] .chat-input-wrap[data-folded] .chat-toolbar-row > :not(.composer-menu),
    html[data-layout="stack"] .chat-input-wrap[data-fold]:not([data-folded]) .chat-toolbar-row > :not(.composer-menu) {
        transition: none;
    }
}

html[data-layout="stack"] .chat-send-inline {
    padding-left: 11px;
    padding-right: 11px;
}

/* Main's transcript clears the taller phone header and the absolute composer. */
html[data-layout="stack"] #chat-messages {
    padding-top: var(--chat-header-reserve, 80px);
    padding-bottom: 0;
    overflow-x: hidden;
}

html[data-layout="stack"] #chat-messages::after {
    flex-basis: calc(var(--chat-input-reserve, 108px) + var(--inset-bottom) - 8px);
}

/* Keyboard-open layout: only #chat-messages scrolls within visual viewport. */
html.keyboard-open,
body.keyboard-open {
    position: fixed;
    inset: 0;
    width: 100%;
    height: var(--vvh);
    max-height: var(--vvh);
    overflow: hidden;
    overscroll-behavior: none;
}

body.keyboard-open #content {
    position: fixed;
    inset: 0;
    height: var(--vvh);
    max-height: var(--vvh);
    padding-left: 0;
    padding-bottom: 0;
    overflow: hidden;
    overscroll-behavior: none;
}

body.keyboard-open #page-chat.active {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--vvh);
    max-height: var(--vvh);
    display: flex;
    flex-direction: column;
    z-index: 20;
    overflow: hidden;
    overscroll-behavior: none;
}

body.keyboard-open #page-chat.active .chat-page-header,
body.keyboard-open #page-chat.active .chat-page-note,
body.keyboard-open .project-panel .project-panel-bar {
    flex-shrink: 0;
    position: relative;
    top: auto;
}

body.keyboard-open #page-chat.active #chat-messages,
body.keyboard-open .chat-instance-panel .chat-messages {
    flex: 1;
    min-height: 0;
    position: relative;
    height: auto;
    max-height: none;
    padding-top: 0;
    padding-bottom: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}

/* The composer is in flow: remove the item, including its column gap. */
body.keyboard-open #page-chat.active #chat-messages::after,
body.keyboard-open .chat-instance-panel .chat-messages::after {
    content: none;
}

body.keyboard-open #page-chat.active #chat-input-area,
body.keyboard-open .chat-instance-panel .chat-input-area {
    flex-shrink: 0;
    position: relative;
    bottom: auto;
    padding-top: 8px;
    padding-bottom: 8px;
}

/* ===========================================================================
   Multi-project sidebar + right thread panel (v6.32.0)
   =========================================================================== */

/* The middle zone: the Projects header pinned at its top, the list below it the sidebar's ONE scroller. A window
   too short for it scrolls the whole column instead (the head stays sticky). */
.nav-projects {
    display: flex;
    flex-direction: column;
    gap: 1px;
    width: 100%;
    align-items: stretch;
    margin: var(--space-1) 0 var(--space-2);
    /* As tall as its rows, shrinking to scroll them when they do not fit: the tools follow it, so a short or
       collapsed list leaves its room above the foot, never between the list and the tools. */
    flex: 0 1 auto;
    min-height: 0;
}

.nav-projects-list {
    display: flex;
    flex-direction: column;
    gap: 1px;
    width: 100%;
    align-items: stretch;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    touch-action: pan-y;
    scrollbar-width: thin;
}

.nav-projects-list[hidden] {
    display: none;
}

/* Find a project (owner 4.11, A1 refined; projects_nav.js): one floating capsule a Project row high (32 on the desk,
   44 on touch) in the header pills' material, --chip-float standing on --elev-rest. It is built as the composer's
   field is (DESIGN "The composer field"): the capsule is the object, the words' row has no surface, and focus is a
   ring drawn as shadows, so no border adds to its height. The magnifier takes the rows' icon column at the house
   glyph size, the words start on the rows' label column, and × (while there is a query) closes the row in the +
   column. --space-1 after the header row, --space-2 before the first row: the step the pins keep before the rest. */
.nav-projects-find {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    width: 100%;
    min-height: var(--nav-item-h);
    margin: var(--space-1) 0 var(--space-2);
    border-radius: var(--pill-radius);
    background: var(--chip-float);
    box-shadow: var(--elev-rest);
    outline: 1px solid transparent;
    outline-offset: -1px;
    transition: var(--motion-tone);
}
.nav-projects-find[hidden] { display: none; }
.nav-projects-find:has(> .nav-find-input:focus) {
    outline-width: 2px;
    outline-offset: -2px;
    box-shadow: inset 0 0 0 1px var(--focus-accent-border), 0 0 0 3px var(--focus-accent-ring), var(--elev-rest);
}

.nav-find-glyph {
    display: inline-flex;
    flex: 0 0 18px;
    align-items: center;
    justify-content: center;
    height: 18px;
    margin-left: calc(var(--space-3) + 1px);  /* the rows' icons: their 1px border, then --space-3 */
    color: var(--text-secondary);
    pointer-events: none;
}
.nav-find-glyph > svg { width: 18px; height: 18px; }

.nav-projects-find > .nav-find-input {
    flex: 1 1 auto;
    align-self: stretch;
    min-width: 0;
    padding: 0 var(--space-1) 0 var(--space-3);
    border: 0;
    border-radius: var(--pill-radius);
    background: transparent;
    box-shadow: none;
}
.nav-projects-find > .nav-find-input:focus { box-shadow: none; }
.nav-find-input::-webkit-search-cancel-button,
.nav-find-input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; display: none; }
/* A finger's field keeps the composer's 16px, the size a phone does not zoom into on focus. */
@media (pointer: coarse) {
    .nav-projects-find > .nav-find-input { font-size: var(--type-section); }
}

.nav-projects-find > .nav-find-clear { flex: 0 0 auto; border-radius: var(--pill-radius); }
.nav-projects-find > .nav-find-clear[hidden] { display: none; }
.nav-find-clear > svg { width: 16px; height: 16px; }

/* The find's spoken count and outcome, for a screen reader only (the list shows them): one clipped pixel, no room. */
.nav-find-status {
    flex: 0 0 1px;
    width: 1px;
    height: 1px;
    margin-left: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* Beyond the feed (a find or a browse) the list keeps the row in view in place itself (projects_nav.js), so the
   engine's own scroll anchoring, which WebKit lacks, never adds a second correction. */
.nav-projects-list[data-beyond] { overflow-anchor: none; }

/* While finding, a found name may take three lines, so the words that matched are not cut off behind the ellipsis
   (the resting list keeps one line). The row grows; its mark keeps its column, centred on the row. */
.nav-projects-list[data-finding] .nav-project-row { padding-block: 5px; }
.nav-projects-list[data-finding] .nav-project-row > .nav-row-label {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    white-space: normal;
    overflow-wrap: anywhere;
    line-height: 1.35;
}

/* The list's own commands (Show more, Clear search, Show all projects, Try again) and its one sentence take a
   Project row's geometry in the meta ink, their words on the label column (Show more's chevron in the icon column),
   so none reads as one more Project. */
.nav-projects-more {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex: 0 0 auto;
    width: 100%;
    min-height: var(--nav-item-h);
    margin-top: var(--space-1);
    padding: 0 var(--space-1) 0 var(--nav-label-x);
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    background: transparent;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--type-meta);
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}
.nav-projects-more:has(> svg) { padding-left: var(--space-3); }
.nav-projects-more > svg { flex: 0 0 18px; width: 18px; height: 18px; }
.nav-projects-more + .nav-projects-more { margin-top: 0; }
.nav-projects-more[aria-busy="true"] { cursor: progress; }
@media (hover: hover) {
    .nav-projects-more:hover {
        background: var(--tone-quiet-hover);
        color: var(--text-primary);
    }
}
.nav-projects-more:focus { outline: none; }
.nav-projects-more:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
    color: var(--text-primary);
}

.nav-projects-note {
    margin: var(--space-1) 0 0;
    padding: var(--space-1) var(--space-2) var(--space-1) calc(var(--nav-label-x) + 1px);  /* the rows' words: their 1px border */
    color: var(--text-meta);
    font-size: var(--type-body);
    line-height: var(--line-body);
    overflow-wrap: anywhere;
}

/* Project rows read as a compact list nested under the "Projects" header (no
   glyph/dot, label indented to align under the parent label, shorter rows and
   no left active bar) — a dropdown list, not standalone tabs. */
.nav-project-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--nav-kebab-w);
    align-items: center;
    width: 100%;
    border-radius: var(--radius-inner);
}

/* Pinned Projects lead the same list: the first row shown after them keeps one step of air, the way the sidebar's
   zones part. No caption, glyph, tint, rule or column, so nothing reads as one more Project (DESIGN "Shell"). */
.nav-project-item[data-after-pins] {
    margin-top: var(--space-2);
}

/* A Project row reads in the secondary ink at the body size, read or not: its new messages are its mark's hue. */
.nav-project-row {
    min-height: var(--nav-item-h);
    font-size: var(--type-body);
    color: var(--text-secondary);
    padding-left: var(--nav-label-x);
    padding-right: var(--space-1);
    border-radius: var(--radius-inner);
    /* One trailing column per row for its one status mark, always reserved (the global [hidden] rule removes a row's
       mark at rest from layout, so the track, not the child, holds the slot): every mark keeps one x across rows,
       whatever the name length and while the sibling ⋯ (the item's own column) is hovered or focused. 18px is the
       widest mark's, three 4px dots and two 3px gaps. */
    display: grid;
    grid-template-columns: minmax(0, 1fr) 18px;
    column-gap: var(--space-2);
}

.nav-project-row > .nav-row-label { grid-column: 1; }
.nav-project-row > .nav-mark { grid-column: 2; }

/* A deleting row states one trailing fact, `Deleting…`, in its wider sibling column. Its census mark stays in the
   accessible name but is not drawn (app.js): motion on a row the owner cannot open would read as work to look at. */
.nav-project-item.is-deleting .nav-project-row { grid-template-columns: minmax(0, 1fr); }

@media (hover: hover) {
    .nav-project-row:not(.active):hover {
        color: var(--text-primary);
    }
}

.nav-project-item.is-deleting .nav-project-row,
.nav-project-item.is-deleting .nav-project-row:disabled {
    cursor: not-allowed;
    color: var(--text-muted);
    opacity: 0.62;
}

.nav-project-item.is-deleting {
    grid-template-columns: minmax(0, 1fr) auto;
}

.nav-project-deleting-status {
    justify-self: end;
    padding-right: 3px;
    color: var(--text-meta);
    font-size: var(--type-meta);
    white-space: nowrap;
}

/* The Projects header: the toggle, the unread count, its status mark, then "+" (New project). */
/* The Projects header keeps Main's columns (owner 19.10, Q1 = B): the toggle (icon, word, chevron), then its facts,
   the count and the mark in the number and mark columns of Main and the rows, and + in the ⋯ column. On touch, where
   rows have no ⋯ column, + follows the chevron so the facts keep those columns. A press on the facts toggles too. */
.nav-projects-header {
    display: flex;
    align-items: center;
    width: 100%;
}

.nav-projects-header > .nav-section-toggle {
    flex: 0 1 auto;
    width: auto;
    min-width: 0;
    padding-right: var(--space-2);
    border-radius: var(--radius-inner) 0 0 var(--radius-inner);
}

.nav-projects-facts {
    order: 1;
    flex: 1 0 auto;
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-3);
    /* A Project row ends its mark column one border and --space-1 before its edge; the facts end where it does. */
    padding-right: calc(var(--space-1) + 1px);
    border-radius: 0 var(--radius-inner) var(--radius-inner) 0;
    color: var(--text-secondary);
    cursor: pointer;
    transition: var(--motion-tone);
}

.nav-projects-facts > .nav-mark-slot {
    flex: 0 0 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.nav-projects-add { order: 2; flex-shrink: 0; }  /* the ⋯ column keeps its width in a narrow sidebar */

@media (pointer: coarse) {
    .nav-projects-add { order: 0; }
}

@media (hover: hover) {
    .nav-projects-facts:hover,
    .nav-projects-header > .nav-section-toggle:has(~ .nav-projects-facts:hover),
    .nav-projects-header > .nav-section-toggle:hover ~ .nav-projects-facts {
        background: var(--tone-quiet-hover);
        color: var(--text-primary);
    }
}

.nav-projects-header .nav-chevron {
    display: inline-flex;
    width: 16px;
    height: 16px;
    transform: rotate(90deg);
    transition: transform 0.15s ease;
}
.nav-projects-header .nav-chevron > svg { width: 16px; height: 16px; }

.nav-projects-header .nav-section-toggle[aria-expanded="false"] .nav-chevron {
    transform: rotate(0deg);
}

.nav-projects-header .nav-row-meta:not(:empty) {
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: var(--pill-radius);
    background: var(--tone-crimson);
    color: var(--tone-crimson-ink);
    font-size: var(--type-meta);
    font-weight: 600;
    line-height: 18px;
    text-align: center;
}

.nav-projects-add {
    width: var(--nav-control-h);
    height: var(--nav-control-h);
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: var(--radius-inner);
    background: transparent;
    color: var(--text-meta);
    cursor: pointer;
}

@media (hover: hover) {
    .nav-projects-add:hover {
        color: var(--text-primary);
        background: var(--tone-quiet-hover);
    }
}
.nav-projects-add:focus-visible {
    color: var(--text-primary);
    background: var(--tone-quiet-hover);
}

.nav-projects-add:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}

/* A row's ⋯: under a mouse it appears with the row's hover or focus. On touch the row's menu opens by a long press
   (owner 19.10, 5A; row_menu_gesture.js), so the ⋯ leaves the row's look and its column there, and stays the keyboard's
   and assistive technology's door: it comes back with its column whenever the keyboard reaches it. */
.nav-project-kebab {
    width: var(--nav-control-h);
    height: var(--nav-control-h);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-meta);
    cursor: pointer;
    border-radius: var(--pill-radius);
}

.nav-project-kebab > svg {
    width: 18px;
    height: 18px;
}

@media (hover: hover) {
    .nav-project-kebab { opacity: 0; }
    .nav-project-item:hover .nav-project-kebab,
    .nav-project-item:focus-within .nav-project-kebab {
        opacity: 1;
    }
    .nav-project-kebab:hover {
        color: var(--text-primary);
        background: var(--tone-quiet-hover);
    }
}

.nav-project-kebab:focus-visible {
    color: var(--text-primary);
    background: var(--tone-quiet-hover);
}

/* Hover paint is not a focus ring: the kebab had only the paint, so a keyboard
   user could not tell it was focused (docs/DESIGN.md "Focus"). */
.nav-project-kebab:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}

/* Touch: a long press opens the row's menu, so the ⋯ takes no room and no finger, and the row's words are not a
   selection or a callout under the held finger. The ⋯ stays in the tab order: focused (the keyboard, or assistive
   technology moving focus), it is drawn again with its column, and it stays drawn while the menu it opened is open. */
@media (pointer: coarse) {
    #primary-sidebar { --nav-kebab-w: 0px; }
    .nav-project-item .nav-project-kebab:not(:focus, [aria-expanded="true"]) {
        width: 0;
        height: 0;
        padding: 0;
        border: 0;
        overflow: hidden;
        opacity: 0;
        pointer-events: none;
    }
    .nav-project-item:has(> .nav-project-kebab:is(:focus, [aria-expanded="true"])) {
        grid-template-columns: minmax(0, 1fr) var(--nav-control-h);
    }
    .nav-project-item {
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        user-select: none;
    }
}

/* A Project row menu's first line: the row's mark and its words (owner 19.10, 5A), the state word in the primary ink.
   It is no item (no hover, no focus); space, not a rule, parts it from the items. */
.project-row-menu-status {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    max-width: 280px;
    margin-bottom: var(--space-1);
    padding: var(--space-2) var(--space-3);
    color: var(--text-secondary);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.project-row-menu-status > .status-mark {
    flex: 0 0 18px;
}
.project-row-menu-word {
    color: var(--text-primary);
    font-weight: 500;
}

/* New Project dialog */
.new-project-dialog {
    max-width: 560px;
    width: min(560px, 94vw);
}

/* The dialog body's gap parts its fields; none adds a margin of its own. */
.new-project-field {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    font-size: var(--type-body);
}

/* The working folder is a field like the name (U023): its label, its options as one wrapping row of the shared
   crimson-accent radios (ui.css .ui-checkbox), no outlined box, and one line under them for the chosen option. */
.new-project-sources { margin: 0; padding: 0; border: 0; min-inline-size: 0; }
.new-project-sources > legend { padding: 0; margin-bottom: var(--space-1); color: var(--text-meta); font-size: var(--type-body); }
.new-project-source-choices { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); color: var(--text-primary); }
.new-project-source-choices > label { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; }

.new-project-selected-target { overflow-wrap: anywhere; }

.new-project-browser {
    border: 1px solid var(--divider);
    border-radius: var(--radius-inner);
    margin-bottom: var(--space-2);
    overflow: hidden;
}

.new-project-browser-path {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 6px 10px;
    border-bottom: 1px solid var(--divider);
    font-size: 12px;
}
.new-project-browser-path > strong { min-width: 0; overflow-wrap: anywhere; }
.new-project-browser-path > button { flex: 0 0 auto; white-space: nowrap; }


.new-project-browser-list {
    display: flex;
    flex-direction: column;
}

.new-project-dir-row {
    text-align: left;
    padding: 6px 10px;
    border: none;
    background: transparent;
    color: var(--text-primary);
    font-size: var(--type-body);
    cursor: pointer;
}

.new-project-dir-row:hover {
    background: var(--menu-item-hover);
}

.new-project-dir-up {
    color: var(--text-muted);
}

.new-project-dir-empty {
    padding: 8px 10px;
    color: var(--text-muted);
    font-size: 12px;
}

/* A warning on a row whose body cannot show it (DESIGN principle 3): the warning's tint in light, a neutral body with
   the warning's words on dark; no outline. */
.new-project-trust-note {
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-inner);
    background: var(--status-warn-bg);
    color: var(--status-warn-word, var(--text-primary));
    font-size: var(--type-meta);
    margin: 0;
}

.project-panel {
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: var(--drag-width, var(--project-panel-width));
    z-index: var(--z-project-panel);
    border-left: 1px solid var(--divider);
    /* A reading surface is one flat opaque material, even where native backdrop blur is not composited; the
       project's colour lives in its small marks, and underlying Main text must never show through. */
    background: rgb(var(--shell-rgb));
    box-shadow: -24px 0 64px rgba(0, 0, 0, 0.34);
    transform: translateX(18px);
    opacity: 0;
    pointer-events: none;
    transition: transform 180ms ease, opacity 180ms ease;
}

.project-panel.open {
    transform: translateX(0);
    opacity: 1;
    pointer-events: auto;
}

.project-panel[hidden] {
    display: none;
}

/* The Project panel's bar is the one page header (app.js renders it); its capsules float over the transcript. */
.project-panel-bar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: var(--z-page-header);
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: space-between;
    padding: 10px 14px;
    border-bottom: none;
    background: transparent;
    pointer-events: none;
}
.project-panel-bar > * {
    pointer-events: auto;
}

/* The bars' icon controls wear the one focus ring (DESIGN §3 Focus), like every other control. */
.project-panel-close:focus-visible,
.mobile-nav-toggle:focus-visible,
.nav-drawer-close:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

.project-panel-body {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    position: relative;  /* a Project room that leaves lies over the arriving one (shell_motion.js) */
}

.project-panel-backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--z-project-scrim);
    cursor: default;
    background: var(--scrim);
    backdrop-filter: blur(1.5px);
    -webkit-backdrop-filter: blur(1.5px);
    opacity: 0;
    pointer-events: none;
    transition: opacity 180ms ease;
}

.project-panel-backdrop.open {
    opacity: 1;
    pointer-events: auto;
}

.project-panel-backdrop[hidden] {
    display: none;
}

.chat-instance-panel {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    /* Own the (now flow) composer + any absolute children so they can never
       escape into the surrounding panel/app the way the main chat's absolute
       overlay header/composer assume a positioned `.page`. */
    position: relative;
}

.chat-instance-panel[hidden] {
    display: none;
}

/* Lean status-only header: the project title + close live in the
   .project-panel-bar above, so the instance only needs its live status. */
.chat-panel-statusbar {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    flex: 0 0 auto;
    padding: 8px 14px 0;
}

.project-panel .chat-panel-statusbar {
    padding-top: var(--chat-header-reserve, 56px);
}

.chat-instance-panel .chat-messages {
    flex: 1;
    overflow-y: auto;
    min-height: 0;
    padding: 8px 14px;
    padding-bottom: 0;
}
.project-panel .chat-instance-panel .chat-messages {
    padding-top: var(--chat-feed-reserve, var(--chat-header-reserve, 56px));
    /* Reserve the real scrollbar lane from the first layout: WebKit's auto-overflow transition can leave rows
       wider than the content box until a resize, covering the focus corners. Overlay scrollbars take no space. */
    overflow-y: scroll;
}
/* The open room's feed holds the keys (app.js focusProjectRoom). Its keyboard focus is two corners of the focus ring,
   never a frame round the room: each 11px outside the rows' edge, in the feed's own gutter, 4px above the first row
   and opening toward it; rows scroll under them, and a pointer quiets them until the next key (chat.js; DESIGN §3
   Focus). Both engines place a background over the scrollbar's lane, so the right pair moves in by the lane the feed
   actually draws (--chat-feed-lane, measured with the room's other reserves; none where scrollbars overlay). */
.project-panel .chat-messages:focus { outline: none; }
.project-panel .chat-messages:focus-visible:not(.is-quiet-focus) {
    background-image: linear-gradient(var(--focus-accent-border), var(--focus-accent-border)),
        linear-gradient(var(--focus-accent-border), var(--focus-accent-border)),
        linear-gradient(var(--focus-accent-border), var(--focus-accent-border)),
        linear-gradient(var(--focus-accent-border), var(--focus-accent-border));
    background-origin: content-box;
    background-repeat: no-repeat;
    background-size: 8px 2px, 2px 12px, 8px 2px, 2px 12px;
    background-position: left -11px top -4px, left -11px top -4px,
        right calc(var(--chat-feed-lane, 0px) - 11px) top -4px, right calc(var(--chat-feed-lane, 0px) - 11px) top -4px;
}
/* Forced colours drop gradients, the corners with them: there the keys get the system's ring, inset. */
@media (forced-colors: active) {
    .project-panel .chat-messages:focus-visible:not(.is-quiet-focus) {
        outline: 2px solid Highlight;
        outline-offset: -2px;
    }
}
/* The note already reserves the floating bar. With a keyboard both are in flow. The room asks and its feed inherits
   the answer: :has() stays in its subject compound (DEVELOPMENT "Design System"). */
.project-panel .chat-instance-panel:has(> .chat-panel-statusbar > :not([hidden])) {
    --chat-feed-reserve: 8px;
}
body.keyboard-open .project-panel .chat-panel-statusbar { padding-top: 8px; }
body.keyboard-open .project-panel .chat-instance-panel .chat-messages {
    padding-top: 0;
}

/* The Project composer already includes its safe-area inset in the measured
   reserve; preserve the panel's own reserve on narrow screens as well. */
.chat-instance-panel .chat-messages::after {
    flex-basis: calc(var(--chat-input-reserve, 108px) - 8px);
}

/* Shared dock contract (v6.71.0): the panel composer docks exactly like the
   main chat one — the base .chat-input-area rule supplies position:absolute
   (anchored to the position:relative panel), the SAME bottom fade gradient,
   and the pointer-events pass-through; no second fade layer. */
.chat-instance-panel .chat-input-area {
    padding: 24px 14px max(16px, var(--inset-bottom));
}

/* Docked, the panel lies beside Main at its own width (its handle resizes it apart from the sidebar's), over the
   dimmed room, and never wider than the content area, so it never covers the sidebar (owner 18.10, 4A). Both are
   placed in the columns after the sidebar's (#app), so the room's dimmed edge and the panel's cap follow the
   sidebar's box by layout, frame by frame while it is dragged. */
html[data-layout="docked"] .project-panel-backdrop,
html[data-layout="docked"] .project-panel {
    position: absolute;
    grid-column: 2 / -1;
}
html[data-layout="docked"] .project-panel {
    max-width: 100%;
    /* It comes in from the window's right edge with its room in it and goes back there whole, opaque all the way, on
       the drawer's curve; only its scrim fades (owner 20.10, Q2 A). #app clips what lies past the edge. */
    transform: translateX(100%);
    opacity: 1;
    transition: transform var(--motion-close) var(--ease-sheet);
}
html[data-layout="docked"] .project-panel.open {
    transform: none;
    transition-duration: var(--motion-open);
}
html[data-layout="docked"] .project-panel-backdrop {
    transition: opacity var(--motion-close) ease;
}
html[data-layout="docked"] .project-panel-backdrop.open {
    transition-duration: var(--motion-fade);
}
/* Where the primary input is a finger (html[data-pointer="coarse"]), the docked Project takes the whole content column in
   Main's place, dressed as the phone's room: nothing dimmed to tap by mistake, no edge or shadow over the sidebar, which
   stays beside it and live (its own handle too), and no width handle of the panel's own: the panel fills the column, so
   a drag would move nothing and would store the column's width as the panel width every device of the install shares.
   Its geometry and its slide from the window's right edge stay the docked ones (owner 20.10, Q2 A); a desk with a mouse
   keeps the panel beside Main at its own width (owner 18.10, 4A; 06.11). Nothing here writes the stored width. */
html[data-layout="docked"][data-pointer="coarse"] .project-panel {
    width: 100%;
    border-left: none;
    box-shadow: none;
}
html[data-layout="docked"][data-pointer="coarse"] .project-panel-backdrop,
html[data-layout="docked"][data-pointer="coarse"] .project-panel > .resize-handle {
    display: none;
}

/* The stack layout: the project thread is a full-width layer over the room. It has no outside to tap and dims nothing:
   its scrim would lie over the drawer, which on a phone comes up from under the room it pushes aside. */
html[data-layout="stack"] .project-panel-backdrop {
    display: none;
}
html[data-layout="stack"] .project-panel {
    position: fixed;
    inset: 0;
    width: 100%;
    z-index: var(--z-project-layer);
    border-left: none;
    /* Pushed aside from its left edge, open or not: with the origin only on .open, a Project card closing at
       scale(.96) pivoted about its centre and jumped 8px the frame it lost the class. */
    transform-origin: 0 50%;
    transform: translateY(14px);
    /* It rises on the sheet curve, and moves as the room card with the drawer (above) on the same one. */
    transition: transform var(--drawer-settle, var(--motion-close)) var(--ease-sheet), opacity var(--motion-fade) ease,
        border-radius var(--drawer-settle, var(--motion-close)) var(--ease-sheet),
        box-shadow var(--drawer-settle, var(--motion-close)) ease;
}

html[data-layout="stack"] .project-panel.open {
    transform: translateY(0);
}

/* With the keyboard up a Project room shrinks to the visible viewport, as Main does (body.keyboard-open #page-chat):
   its composer, with the room's ☰, stays above the keyboard instead of behind it. */
body.keyboard-open .project-panel {
    height: var(--vvh);
    max-height: var(--vvh);
}

@media (prefers-reduced-motion: reduce) {
    .project-panel,
    .project-panel-backdrop,
    .chat-scroll-bottom-btn {
        transition: none;
    }
    /* Docked, the panel appears where it stands with a short fade instead of travelling. */
    html[data-layout="docked"] .project-panel {
        transform: none;
        opacity: 0;
        transition: opacity var(--motion-fade) ease;
    }
    html[data-layout="docked"] .project-panel.open { opacity: 1; }
    .chat-load-older[aria-busy="true"] .chat-load-older-btn {
        animation: none;
    }
}

/* Agent accounts (D30).
   Borders read the theme's --divider like every other panel surface. They used
   to name --border, which this dark-only :root never declares, so all three
   rules silently took their literal grey fallback instead of the token. */
/* Subsection rhythm: a group is its head (heading + that group's own Add
   action, docked right — docs/DESIGN.md "List editors") followed by its rows,
   one space token apart inside the group; the section's own flex gap keeps
   the groups apart, so a heading always sits nearer its rows than its
   neighbour — structure, not negative margins. */
/* ONE service banner for the Agents tab: the single place a daemon/runtime
   problem is explained, so no row has to carry "(not in discovery)". */
.agents-service-banner {
    display: flex;
    align-items: baseline;
    padding: 10px 0;
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    background: transparent;
}
/* The banner's row (owner, refined A 21.10): the verdict, the engine version as a quiet fact beside it, and
   Diagnostics, which opens the engine's measurements (home, heap, loop, store, jobs, caps) under the row. */
.agents-service { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--space-3); min-width: 0; }
.agents-service-version { color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.agents-service-diagnostics {
    flex-basis: 100%;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin: 0 0 var(--space-3);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.agents-service-diagnostics[hidden] { display: none; }
.agents-service-diagnostics > div { display: flex; flex-wrap: wrap; gap: 0 var(--space-4); }
.agents-service-diagnostics dt { flex: 0 1 8em; color: var(--text-secondary); }
.agents-service-diagnostics dd { flex: 1 1 240px; margin: 0; min-width: 0; color: var(--text-primary); overflow-wrap: anywhere; }
/* A quiet disclosure (Diagnostics, a program's Details): its words and a chevron that turns when open; no body at
   rest, the quiet tint under a hovering pointer, the shared ring for the keyboard. The pill's padding is paid back by
   an equal negative margin, so it stands where its words stand. */
.settings-disclosure {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    margin: calc(var(--space-1) * -1) calc(var(--space-2) * -1);
    padding: var(--space-1) var(--space-2);
    border: 0;
    border-radius: var(--pill-radius);
    background: transparent;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    font-weight: 600;
    cursor: pointer;
    transition: var(--motion-tone);
}
.settings-disclosure[hidden] { display: none; }
.settings-disclosure:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
/* Under a finger it is a disclosure's summary: its words are the touch minimum tall, standing in their own place. */
@media (pointer: coarse) {
    .settings-disclosure { min-height: var(--touch-target-min); margin-block: 0; }
}
@media (hover: hover) {
    .settings-disclosure:hover { background: var(--tone-quiet-hover); }
}
.settings-disclosure-caret { flex: none; transition: transform 0.18s ease; }
.settings-disclosure[aria-expanded="true"] .settings-disclosure-caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .settings-disclosure-caret { transition: none; } }
/* Who an unpinned run would take next: a quiet fact of the family head, not a second status. */
.agent-family-next { color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
/* Accounts are grouped by FAMILY, one card each (owner: "все акки клод кода
   должны быть эквивалентны", "в каждой секции добавить кнопку"). The card
   header carries the family name, its aggregate status and its OWN Add
   button — the affordance used to hang off one privileged row. */
.agent-family-list { display: flex; flex-direction: column; gap: 12px; }
.agent-family-card {
    padding: 14px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.agent-family-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}
.agent-family-id { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; min-width: 0; }
.agent-family-id h4 {
    margin: 0;
    font-size: var(--type-section);
    line-height: var(--line-title);
    font-weight: 600;
    color: var(--text-primary);
}
/* Harness identity is one shared mark + visible-label primitive. Geometry
   stays in harness_presentation.js; CSS owns only layout, inherited ink and
   long-label behavior. The mark never steals width from the readable name. */
.harness-identity {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-width: 0;
    max-width: 100%;
    vertical-align: middle;
}
.harness-identity-mark {
    inline-size: 1em;
    block-size: 1em;
    flex: 0 0 1em;
    color: currentColor;
    fill: currentColor;
}
.harness-identity-label {
    min-width: 0;
    overflow-wrap: anywhere;
}
.available-subagent-route-identity-wrap {
    display: flex;
    align-items: center;
    min-width: 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.agent-family-rows { display: flex; flex-direction: column; gap: 8px; }
.harness-maintenance-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1) var(--space-2);
    min-width: 0;
}
.harness-maintenance-line {
    flex: 1 1 auto;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
    overflow-wrap: anywhere;
}
.harness-maintenance-actions { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.harness-maintenance-notice,
.harness-maintenance-details {
    flex-basis: 100%;
    min-width: 0;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
    overflow-wrap: anywhere;
}
.harness-maintenance-notice[data-tone="warn"] { color: var(--status-warn-fg); }
/* The program's state row: its status, a short cause or this view's own sentence, and the Details toggle. */
.harness-maintenance-state {
    flex-basis: 100%;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-1) var(--space-3);
    min-width: 0;
}
.harness-maintenance-state > .harness-maintenance-notice { flex-basis: auto; }
.harness-maintenance-cause { min-width: 0; color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.harness-maintenance-details[hidden] { display: none; }
.harness-maintenance-details > p { margin: var(--space-2) 0 0; }
.harness-maintenance-exact code { white-space: pre-wrap; overflow-wrap: anywhere; }
.harness-maintenance-details dl { margin: var(--space-2) 0; }
.harness-maintenance-details dt { color: var(--text-meta); }
.harness-maintenance-details dd { margin: 0 0 var(--space-1); color: var(--text-primary); }
.harness-maintenance-details pre { white-space: pre-wrap; overflow-wrap: anywhere; }
/* Account rows are purpose-built (identity left, actions right), not the
   settings key/value grid — .settings-custom-secret-row's 0.65fr/1.35fr
   columns inverted the proportions and its align-items:end sank the text.
   Two lines: the account and its status on top, muted metadata below, actions
   docked right across both (docs/DESIGN.md "Account group / row anatomy"). No
   row is privileged: the default CLI login uses this exact layout. */
.harness-account-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "main actions" "meta actions";
    align-items: center;
    column-gap: 12px;
    row-gap: 2px;
    padding: 10px 12px;
}
.harness-account-main {
    grid-area: main;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: baseline;
    min-width: 0;
}
.harness-account-meta {
    grid-area: meta;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    min-width: 0;
}
.harness-account-actions { grid-area: actions; display: flex; gap: 6px; flex-wrap: wrap; }
/* Row anatomy: name = the one primary thing (docs/DESIGN.md). */
.harness-account-main strong {
    font-size: var(--type-body);
    line-height: var(--line-body);
    font-weight: 600;
    color: var(--text-primary);
}
/* Status inside a family card or an account row is a meta-sized dot + text, on
   the tab's own scale — the global .ui-status chrome size belongs to surfaces
   that have not migrated yet. */
.agent-family-id .ui-status,
.harness-account-main .ui-status {
    display: inline-flex;
    align-items: center;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.agent-family-id .ui-status[data-tone]::before,
.harness-account-main .ui-status[data-tone]::before {
    margin-right: 6px;
}
/* Narrow viewports: the actions column would squeeze the account name to a
   couple of characters, so the row becomes one column and the buttons drop
   under the metadata they act on. */
@media (max-width: 980px) {
    .harness-account-row {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "main" "meta" "actions";
        row-gap: 8px;
    }
    .agent-family-head { align-items: flex-start; }
}
/* Classification chip: neutral pair, never inherited ink (docs/DESIGN.md).
   Also reused by Chat's live executor chip, which keeps its own 10px sizing. */
.harness-chip {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 999px;
    /* A classification chip is a flat tint (DESIGN §4 "The tone primitive"): no outline, no tracking. */
    background: var(--status-neutral-bg);
    color: var(--status-neutral-fg);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.log-executor-chip {
    width: fit-content;
    max-width: 100%;
}
/* The meta line is facts: each one whole on its line (a fact wider than the line wraps inside itself), parted by a
   dot that stays with the fact before it, so no line starts with a separator. A spent window is the one fact in the
   warning's hue (DESIGN principle 3: the words that carry the state take the hue; the row's body stays neutral, so
   rows never alternate); the plan, who it is and "checked …" keep the meta ink. */
.harness-account-meta .meta-fact { display: inline-block; max-width: 100%; overflow-wrap: anywhere; }
.harness-account-meta .meta-fact:not(:last-of-type)::after { content: " ·"; margin-right: 0.35em; color: var(--text-meta); }
.harness-account-meta .meta-fact[data-tone="warn"] { color: var(--status-warn-word, var(--text-meta)); }
/* The light seed's half of the warning row (DESIGN seed table: light — the row's tint, its words in their own ink;
   dark — a neutral row, its words in the hue): the one warning row with a body, so the theme scopes its rule here. */
:root[data-theme="light"] .harness-account-row.harness-exhausted { background: var(--status-warn-bg); }
/* "Checking…" keeps the inspect button's width, so a press resizes nothing (DESIGN principle 6). */
.harness-maintenance-actions [data-maintenance-action="inspect"] { min-width: 7.5em; }
/* The login card frames itself: .panel-card is an onboarding.css class the
   settings pages never load, so leaning on it rendered a bare, frameless
   card (finding #4). Same ui-card tokens as every other panel surface. */
.harness-login-card {
    margin-top: 8px;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    padding: 16px 18px;
}
.harness-login-card h4 {
    margin: 0;
    font-size: var(--type-body);
    line-height: var(--line-title);
    font-weight: 600;
    color: var(--text-primary);
}
.harness-login-card p { margin: 0; color: var(--text-meta); font-size: var(--type-body); line-height: var(--line-body); }
.harness-device-code p { margin: 4px 0; }
/* Device code: display size is the point; nearest token, not a one-off 22px. */
.harness-code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
    font-size: var(--type-page);
    letter-spacing: 0.18em;
    padding: 8px 12px;
    border: 1px dashed var(--divider);
    border-radius: 8px;
    display: inline-block;
    user-select: all;
}
.harness-attach-command {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
    font-size: var(--type-meta);
    padding: 8px 10px;
    border-radius: 8px;
    background: var(--surface-tray);
    overflow-x: auto;
    user-select: all;
    white-space: pre-wrap;
    word-break: break-all;
}
/* Link-first login card (no-terminal UX): the sign-in link is the prominent
   action; copy stays an explicit secondary affordance. */
.harness-signin-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.harness-signin-actions a.btn { text-decoration: none; }
.harness-code-entry { display: flex; flex-direction: column; gap: 6px; }
.harness-code-entry label { font-size: var(--type-meta); line-height: var(--line-meta); color: var(--text-meta); }
.harness-code-entry-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.harness-code-entry-row input { width: auto; min-width: min(220px, 100%); flex: 1 1 220px; }
/* The demoted attach fallback: collapsed by default, visually secondary. */
.harness-advanced {
    align-self: stretch;
    border-top: 1px solid var(--divider);
    padding-top: 8px;
}
.harness-advanced summary {
    cursor: pointer;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
}
.harness-advanced[open] summary { margin-bottom: 6px; }
.harness-advanced p { margin: 4px 0; }

/* --- Dashboard › Updates (updates.js, update_versions.js) on the Dashboard tab body ---
   One card: the channel's state as the tab's headline (its one status mark, its facts on one line, one next action
   beside it), then the last attempt, the record's facts, the description and Recovery, each behind its disclosure. */
.updates-card { display: flex; flex-direction: column; gap: var(--space-3); }
.updates-hint {
    margin: 0;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
}
/* The last attempt is one quiet line of the record; open, it takes the row, and its Restore keeps its end. */
.updates-attempt { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); container: updatesattempt / inline-size; }
/* Words of an outcome that is not a plain success stay in view under the line (a success keeps them in its Details). */
.updates-attempt > .updates-hint { flex: 1 1 100%; }
.updates-attempt-details { min-width: 0; }
/* The summary's words wrap as one line: the time never breaks, and on a narrow card the outcome takes the next line
   under the words, not under the caret. */
.updates-attempt-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--space-2); min-width: 0; }
.updates-attempt-line > time { white-space: nowrap; }
.updates-attempt-details[open] { flex: 1 1 100%; }
.updates-attempt > .btn { margin-inline-start: auto; }
/* A narrow attempt (under 34rem: a phone, or a window up to about 640px) puts Restore under the summary at its start,
   closed or open (approved A). */
@container updatesattempt (max-width: 34rem) {
    .updates-attempt-details { flex-basis: 100%; }
    .updates-attempt > .btn { margin-inline-start: 0; }
}
/* A group heading inside a facts list (Before / After the update): the list's meta ink, semibold. */
.updates-facts-head {
    grid-column: 1 / -1;
    color: var(--text-meta);
    font-weight: 600;
    margin-top: var(--space-2);
}
.updates-facts-head:first-child { margin-top: 0; }
.updates-recovery {
    border-top: 1px solid var(--divider);
    padding-top: var(--space-3);
}
.updates-recovery-copy {
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
    margin: 0 0 var(--space-3);
}
.updates-recovery-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}
.updates-branch {
    font-size: var(--type-meta);
    color: var(--text-meta);
    margin: 0 0 var(--space-2);
}
.updates-subhead {
    font-size: var(--type-body);
    font-weight: 600;
    color: var(--text-primary);
    margin: var(--space-4) 0 var(--space-2);
}
.updates-restore-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    max-height: var(--updates-list-max-height);
    overflow-y: auto;
}
.updates-restore-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-inner);
    min-width: 0;
}
.updates-restore-row:hover {
    background: var(--tone-quiet-hover);
}
.updates-restore-label {
    color: var(--text-primary);
    font-weight: 600;
    flex: none;
}
.updates-restore-date {
    color: var(--text-meta);
    flex: none;
}
.updates-restore-msg {
    color: var(--text-meta);
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.updates-restore-row .btn {
    margin-left: auto;
    flex: none;
}
.updates-empty {
    font-size: var(--type-meta);
    color: var(--text-meta);
    padding: var(--space-2);
}
.updates-empty-error {
    color: var(--status-error-fg);
}

/* The update description: Ouroboros's own account of the update this panel
   offers, kept after it lands as "What changed in this version". A fact block,
   so it is separated from the headline the way Recovery is — a rule, not a
   card, with two small actions: Check (facts only) and Write/Rewrite (an
   ordinary task that authors it). */
.updates-letter {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    border-top: 1px solid var(--divider);
    padding-top: var(--space-3);
}
/* The heading and the actions share a row while both fit; otherwise (a phone) the actions
   follow on their own, so the date keeps the width instead of a column beside it. */
.updates-letter-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}
.updates-letter-heading {
    flex: 1 1 auto;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    min-width: 0;
}
.updates-letter-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--space-1);
    margin-left: auto;
}
.updates-letter-refresh,
.updates-letter-write {
    white-space: nowrap;
}
.updates-letter-label {
    margin: 0;
    font-size: var(--type-body);
    line-height: var(--line-title);
    font-weight: 600;
    color: var(--text-primary);
}
/* The real writing time stays quiet; detailed range provenance is disclosed below. */
.updates-letter-meta {
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
    overflow-wrap: anywhere;
    min-width: 0;
}
/* Description freshness is separate from installation status and never a banner. */
.updates-letter-note {
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
}
.updates-letter-details {
    margin-top: var(--space-3);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
    overflow-wrap: anywhere;
}
.updates-letter-details p {
    margin: var(--space-2) 0 0;
}
/* The letter opens at its first part (about eight lines); "Read all" opens the rest in place. */
.updates-letter-body {
    font-size: var(--type-body);
    line-height: var(--line-body);
    color: var(--text-primary);
    margin-top: var(--space-2);
}
.updates-letter-body:not(.is-open) { max-height: calc(8 * var(--line-body) * 1em); overflow: hidden; }
/* A long reading text keeps its structure: the title one size up, a section as a label with room above it, a
   subsection in the secondary ink — declared after the global .md-h* block it narrows. */
.updates-letter-body :is(.md-h1, .md-h2, .md-h3) {
    line-height: var(--line-title);
    color: var(--text-primary);
    font-weight: 600;
    margin: var(--space-2) 0 var(--space-1);
}
.updates-letter-body .md-h1 { font-size: var(--type-section); }
.updates-letter-body .md-h2 { font-size: var(--type-body); margin-top: var(--space-4); }
.updates-letter-body .md-h3 { font-size: var(--type-body); font-weight: 500; color: var(--text-secondary); }


/* The child card's effort chip: one text part of the meta line; a pinned level wears the pin glyph. */
.chat-live-effort {
    display: inline-flex;
    align-items: center;
}
/* The pin keeps its own space (a margin, not a flex gap), so the meta separator that rides inside this fact
   sits as close as after any other fact. */
.chat-live-effort-pin {
    flex: 0 0 auto;
    width: 11px;
    height: 11px;
    margin-inline-end: calc(var(--space-1) * 0.75);
}

/* Phase 6: per-bubble/subagent executor chip (owner directive: a small chip
   like Claudexor's, never a promotional badge). Reuses .harness-chip, sized on
   the shared meta token like the rest of the migrated chat surface. */
.chat-live-executor-chip {
    font-size: var(--type-meta);
    padding: 0 6px;
    /* No trailing margin: the meta line now carries a real ' \00b7 ' text part
       between the chip and the next fact, so this boundary is spaced by the
       same flex gap as every other one. */
    vertical-align: middle;
}

/* Structured chat delivery surfaces. All controls inherit the dark application
   palette so media stays legible in both Main and Project chat instances. */
.chat-bubble { position: relative; }

.chat-gallery-grid,
.chat-file-grid,
.chat-links-list {
    display: grid;
    gap: 8px;
    min-width: 0;
    /* Structural markup, not owner prose: the bubble's `pre-wrap` would turn
       every newline of the card template into a rendered blank line. Captions
       below opt back in, because those line breaks are the owner's. */
    white-space: normal;
}

.chat-bubble.is-multiple .chat-gallery-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* The links bubble's own .message is a container (title + list), so the same
   structural rule applies to it directly — at the specificity of the
   `.chat-bubble .message` pre-wrap default it has to beat. */
.chat-bubble .message.chat-links-message {
    white-space: normal;
}

.chat-group-title,
.chat-links-title {
    margin-bottom: 8px;
    color: var(--text-meta);
    font-size: var(--type-meta);
    font-weight: 600;
}

.chat-gallery-item {
    position: relative;
    min-width: 0;
    margin: 0;
    overflow: hidden;
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    background: var(--surface-tray);
}

/* Whole images on both sides (owner choice): a tile letterboxes, never crops. */
.chat-gallery-item .chat-photo {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: contain;
    border-radius: 0;
}

/* The tile's own button opens the image in the reader; its `•••` menu stays separate. */
.chat-photo-open {
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

.chat-photo-open:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}

/* One photo alone keeps its own ratio up to the token height, whoever sent it:
   an Ouroboros single-photo bubble or an owner bubble with one image. */
.chat-bubble:not(.is-multiple) .chat-gallery-grid:not(.is-multiple) .chat-gallery-item {
    justify-self: start;
    width: fit-content;
    max-width: 100%;
}

.chat-bubble:not(.is-multiple) .chat-gallery-grid:not(.is-multiple) .chat-photo {
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: var(--chat-photo-max-height);
    /* A tiny, very narrow or very short photo still gets a box its always-present
       actions fit in (the figure clips what overflows it); scale-down shows the
       photo whole at its own size and ratio inside that box, never enlarged. */
    min-width: var(--chat-photo-min-box);
    min-height: var(--chat-photo-min-box);
    aspect-ratio: auto;
    object-fit: scale-down;
}

/* Owner attachments: one block above the caption inside the owner's bubble
   (DESIGN "Chat attachments"); the photo grid, players and file cards are the
   shared delivery atoms above, composed — not restyled. */
.chat-bubble.has-attachments {
    max-width: min(80%, var(--chat-media-bubble-max));
}

.chat-bubble.has-attachments.has-wide-media {
    width: min(80%, var(--chat-media-bubble-max));
}

.chat-attachments {
    display: grid;
    /* One column the bubble's width: no child's min-content (a player's control
       row, a long nowrap title) may widen it past the bubble's edge. */
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-2);
    min-width: 0;
    white-space: normal;
}

.chat-attachments + .message { margin-top: var(--space-2); }

.chat-attachments .chat-gallery-grid.is-multiple {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}

.chat-attachments .chat-file-card { min-width: 0; }

.chat-file-item.is-unavailable .chat-file-name { color: var(--text-meta); }
.chat-file-card:disabled .chat-file-more { visibility: hidden; }

.chat-attachment-player .chat-media-player { min-width: 0; }

/* The shared player keeps its controls inside the bubble: they wrap, and a video's
   progress bar takes its own row (the player's narrow layout below). */
.chat-attachment-player .chat-media-controls { flex-wrap: wrap; }
.chat-attachment-player .chat-media-player:not(.is-audio) .chat-media-progress { order: 2; flex-basis: 100%; }

.attach-badge .attach-thumb {
    flex: 0 0 auto;
    width: var(--chat-attach-thumb-size);
    height: var(--chat-attach-thumb-size);
    border-radius: var(--radius-inner);
    object-fit: cover;
}

/* Touch: actions are always visible (no hover), and big enough for a finger. The photo's ••• is a locked .btn, so it
   takes the minimum through its height token (the lock, ui.css); the attachment's × is a plain button. */
@media (pointer: coarse) {
    .chat-photo-actions summary { --control-h: var(--touch-target-min); min-width: var(--touch-target-min); }
    .attach-remove {
        min-width: var(--touch-target-min);
        min-height: var(--touch-target-min);
    }
}

@container chatcol (max-width: 620px) {
    .chat-bubble.has-attachments,
    .chat-bubble.has-attachments.has-wide-media {
        width: auto;
        max-width: 100%;
    }

    .chat-attachments .chat-gallery-grid.is-multiple { grid-template-columns: minmax(0, 1fr); }
}

.chat-gallery-item figcaption,
.chat-file-caption {
    padding: 7px 9px;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
    /* Owner-authored text inside a structural container: keep its line breaks. */
    white-space: pre-wrap;
}

.chat-photo-actions {
    position: absolute;
    top: 7px;
    right: 7px;
    z-index: 2;
}

/* The shared floating capsule (.btn.btn-float.btn-sm) sits over the photo: 30px wide, its dots centred with no inline
   inset (the inset is the control's token: the lock writes the padding). */
.chat-photo-actions summary {
    --control-pad-x: 0;
    display: grid;
    width: 30px;
    place-items: center;
    list-style: none;
}

.chat-photo-actions summary::-webkit-details-marker { display: none; }

/* A file card is the shared neutral capsule (.btn.btn-default) in a card's shape: it carries a name and its facts, so
   it keeps the inner radius and its words' own weights. */
.chat-file-card {
    display: grid;
    grid-template-columns: 48px minmax(0, 1fr) auto;
    gap: 10px;
    align-items: center;
    width: 100%;
    min-width: 230px;
    font: inherit;
    text-align: left;
}
/* A button that carries a paragraph is a card (DESIGN.md principle 4): its own height and inset, the inner radius, the
   bubble's text size — chosen through the control tokens, never by writing the box. */
.chat-file-card { --control-h: auto; --control-pad-y: 9px; --control-pad-x: 9px; --control-radius: var(--radius-inner); --button-font-size: 1em; font-weight: inherit; }

.chat-file-thumb,
.chat-file-glyph {
    width: 48px;
    height: 48px;
    border-radius: var(--radius-inner);
}

.chat-file-thumb { object-fit: cover; }

.chat-file-glyph {
    display: grid;
    place-items: center;
    background: var(--accent-12);
    color: var(--accent-light);
    font-size: var(--type-page);
}

.chat-file-copy { min-width: 0; }
.chat-file-name,
.chat-file-meta { display: block; }

/* Either direction keeps a long name on two lines; the dialog shows it whole. */
.chat-file-name {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    color: var(--text-primary);
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: normal;
    overflow-wrap: anywhere;
}

.chat-file-meta {
    margin-top: 2px;
    color: var(--text-meta);
    font-size: var(--type-meta);
}

.chat-file-more { color: var(--text-meta); }

/* The file dialog and the document reader (below) are one modal surface; the
   buttons of both are `.btn.btn-default`. */
.chat-file-dialog,
.document-reader {
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-sheet);
    background: var(--ui-modal-bg);
    color: var(--text-primary);
    box-shadow: 0 18px 44px rgba(0, 0, 0, 0.34);
}

.chat-file-dialog { width: min(420px, calc(100vw - 32px)); }

/* An engine whose ::backdrop does not inherit custom properties yet (WebKit before 17.4) keeps the dark veil. */
.chat-file-dialog::backdrop,
.document-reader::backdrop { background: var(--scrim, rgba(0, 0, 0, 0.45)); }

.chat-file-dialog-panel { padding: 18px; }

.chat-file-dialog-title {
    margin-bottom: 16px;
    overflow-wrap: anywhere;
    font-weight: 600;
}

.chat-file-dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

/* The player's buttons are the shared small capsule (.btn.btn-default.btn-sm); its speed list is a field and keeps the
   field's hairline. */
.chat-media-rate-menu {
    min-height: 32px;
    border: 1px solid var(--field-border);
    border-radius: var(--radius-inner);
    background: var(--bg-secondary);
    color: var(--text-primary);
    cursor: pointer;
}

/* Delivered-document reader (document_reader.js, DESIGN "Document reading"):
   a modal reading surface over the conversation, a full sheet on a phone. Its
   chrome is the file dialog's above; its buttons are `.btn.btn-default`. */
.chat-file-more.is-read {
    color: var(--accent-light);
    font-size: var(--type-meta);
    font-weight: 600;
}

.document-reader {
    width: min(var(--document-reader-size), calc(100vw - 2 * var(--space-5)));
    max-width: none;
    /* `vh` first for an engine without `dvh`. */
    height: min(var(--document-reader-size), calc(100vh - 2 * var(--space-5)));
    height: min(var(--document-reader-size), calc(100dvh - 2 * var(--space-5)));
    max-height: none;
    overflow: hidden;
    /* Opaque: the conversation must not show through the text being read. */
    background: var(--bg-primary);
}

.document-reader-panel {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
}

.document-reader-head {
    display: grid;
    grid-template-areas: "identity actions close";
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4) var(--space-3) var(--space-5);
    border-bottom: 1px solid var(--divider);
}

/* Name, type and size are selectable text like the document: declared, so the macOS window's
   title bar band (launcher_window_chrome.py) leaves a press on them to selection. */
.document-reader-identity { grid-area: identity; min-width: 0; cursor: text; }
.document-reader-actions { grid-area: actions; }
.document-reader-close { grid-area: close; }

.document-reader-title {
    margin: 0;
    overflow: hidden;
    color: var(--text-primary);
    font-size: var(--type-section);
    font-weight: 600;
    line-height: var(--line-title);
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The reader's own head: the name and its actions share one wrapping row beside Close, so the
   actions take a line of their own before the name is squeezed, at any width, and the whole
   name wraps instead of being cut (a phone has no hover title). */
.document-reader-head.has-lead {
    grid-template-areas: "lead close";
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
}

.document-reader-lead {
    grid-area: lead;
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--space-2) var(--space-4);
    min-width: 0;
}

.document-reader-lead .document-reader-identity { flex: 1 1 240px; }
.document-reader-lead .document-reader-actions { margin-inline-start: auto; }
.document-reader-head.has-lead .document-reader-title {
    overflow-wrap: anywhere;
    white-space: pre-wrap;
    /* A caption can be paragraphs long. Keep the whole text keyboard-scrollable
       without letting it displace Close, the actions or the reading region. */
    max-height: 20vh;
    max-height: 20dvh;
    overflow: auto;
    overscroll-behavior: contain;
}

.document-reader-head.has-lead .document-reader-title:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

.document-reader-meta {
    margin-top: var(--space-1);
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

/* Two groups, the view and the file's own actions (Copy, Open, Download); a row too short for both breaks between
   them, never inside one while it fits. */
.document-reader-actions,
.document-reader-views,
.document-reader-zoom,
.document-reader-file {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

/* The chosen view: the primary ink on a faint crimson wash (no edge: the lock keeps every control's border clear). */
.document-reader-views .btn[aria-pressed="true"] {
    color: var(--text-primary);
    background: var(--accent-12);
}

/* Opened by a pointer, the focused reading region shows its ring from the first
   key; until then no ring at all, the engine's own included. */
.document-reader-body:focus { outline: none; }

.document-reader-body:focus-visible:not(.is-quiet-focus) {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: -2px;
}

.document-reader-notice {
    padding: var(--space-2) var(--space-5);
    border-bottom: 1px solid var(--status-warn-border);
    background: var(--status-warn-bg);
    color: var(--status-warn-fg);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

.document-reader-notice p { margin: 0; }

.document-reader-body {
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--space-5);
    overflow: auto;
    overscroll-behavior: contain;
}

/* One reading measure, centred; code and tables scroll inside their own wrappers. */
.document-reader-markdown,
.document-reader-source,
.document-reader-status {
    max-width: var(--document-reader-measure);
    margin: 0 auto;
}

.document-reader-markdown {
    color: var(--text-primary);
    font-size: var(--type-section);
    line-height: var(--line-body);
}

.document-reader-markdown p { margin-block: var(--space-3); }
/* Headings take the chat reading ladder (with the rich-answer rules above). */
.document-reader-markdown > :first-child { margin-block-start: 0; }

/* A relative or refused link has no destination here: it reads as its words. */
.document-reader-markdown a.md-link:not([href]) {
    color: inherit;
    text-decoration: none;
}

.document-reader-source {
    margin-block: 0;
    color: var(--text-primary);
    font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
    font-size: var(--type-body);
    line-height: var(--line-body);
    overflow-wrap: anywhere;
    white-space: pre-wrap;
    tab-size: 4;
}

.document-reader-status {
    display: grid;
    justify-items: start;
    gap: var(--space-3);
    color: var(--text-meta);
    font-size: var(--type-body);
    line-height: var(--line-body);
}

.document-reader-status p { margin: 0; }
.document-reader-status.is-error { color: var(--text-primary); }

/* An image: Fit shows it whole and never enlarges it; Actual size scrolls its own pixels. */
.document-reader-body.is-image {
    display: block;
}

.document-reader-image {
    max-width: 100%;
    max-height: 100%;
    margin: auto;
    object-fit: contain;
    user-select: none;
    -webkit-user-drag: none;
}

.document-reader-body.is-actual .document-reader-image { max-width: none; max-height: none; }

/* Image scenes reuse the reader's existing chrome and actions, but let the
   picture occupy the stage. Text, Place and Skill sheets never receive this
   modifier. */
.document-reader.is-image-scene .document-reader-panel { position: relative; }
.document-reader.is-image-scene .document-reader-head {
    position: absolute;
    z-index: 2;
    inset: 0 0 auto;
    border-bottom: 0;
    background: linear-gradient(to bottom, var(--bg-primary), transparent);
}

.document-reader.is-image-scene .document-reader-body.is-image {
    display: flex;
    flex-direction: column;
    min-height: 0;
    padding: var(--space-3) var(--space-4) var(--space-4);
    background: var(--bg-primary);
    overflow: hidden;
}

.document-reader-image-stage {
    position: relative;
    display: grid;
    place-items: center;
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    touch-action: none;
    user-select: none;
}

.document-reader-image-stage .document-reader-image {
    transform: translate3d(var(--image-x, 0px), var(--image-y, 0px), 0)
        scale(var(--image-scale, 1));
    transform-origin: center center;
    cursor: grab;
    will-change: transform;
}

.document-reader-image-stage.is-image-panned .document-reader-image { cursor: grabbing; }

.document-reader-zoom .btn {
    min-width: var(--control-h);
}

/* [hidden] also removes the scene controls from the dialog focus cycle. */
.document-reader.is-image-scene.is-scene-chrome-hidden .document-reader-head { display: none; }

/* A phone reads the document on a full sheet; Close returns to the same place in the chat. */
@media (max-width: 620px) {
    .document-reader {
        width: 100vw;
        height: 100vh;
        height: 100dvh;
        margin: 0;
        border: 0;
        border-radius: 0;
    }

    .document-reader-head {
        grid-template-areas: "identity close" "actions actions";
        grid-template-columns: minmax(0, 1fr) auto;
        padding: calc(var(--space-2) + env(safe-area-inset-top, 0px)) var(--space-3) var(--space-2);
    }

    /* The reader's own head keeps reading first: the whole name beside Close, then every action in sight across the
       sheet's width, the view first and the file's actions after it, one row when both groups fit and one row each
       when they do not. Nothing goes under a menu (DESIGN "Document reading"). */
    .document-reader-head.has-lead { grid-template-areas: "identity close" "actions actions"; }
    .document-reader-head.has-lead .document-reader-lead { display: contents; }
    .document-reader-lead .document-reader-actions { min-width: 0; margin-inline-start: 0; }

    .document-reader-notice { padding-inline: var(--space-3); }

    .document-reader-body {
        padding: var(--space-4) var(--space-3) calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
    }

    .document-reader.is-image-scene .document-reader-body.is-image {
        padding: calc(var(--space-3) + env(safe-area-inset-top, 0px))
            var(--space-3)
            calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
    }
}

.chat-media-player {
    overflow: hidden;
    min-width: 250px;
    /* Structural markup (see .chat-gallery-grid): no phantom lines between the
       stage and the control row. */
    white-space: normal;
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    background: var(--ui-modal-bg);
}

.chat-media-stage video {
    display: block;
    width: 100%;
    max-height: 430px;
    background: var(--bg-primary);
    cursor: pointer;
}

.chat-media-stage audio { display: none; }

.chat-media-title {
    padding: 10px 12px 0;
    overflow: hidden;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.chat-media-player.is-audio { padding-top: 6px; }

.chat-media-controls {
    display: flex;
    gap: 6px;
    align-items: center;
    padding: 8px;
}

.chat-media-controls button {
    flex: 0 0 auto;
    min-width: 32px;
    min-height: 32px;
    padding: 4px 7px;
}

.chat-media-controls button > svg {
    display: block;
    margin: 0 auto;
}

.chat-media-progress {
    --media-progress: 0%;
    flex: 1 1 120px;
    min-width: 70px;
    height: 4px;
    appearance: none;
    border-radius: var(--pill-radius);
    background: linear-gradient(to right, var(--accent-light) var(--media-progress), var(--status-neutral-bg) var(--media-progress));
    cursor: pointer;
}

.chat-media-progress::-webkit-slider-thumb {
    width: 13px;
    height: 13px;
    appearance: none;
    border-radius: 50%;
    background: var(--accent-light);
}

.chat-media-time {
    flex: 0 0 auto;
    color: var(--text-meta);
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
    font-size: var(--type-meta);
    white-space: nowrap;
}

.chat-media-rate-menu { width: 58px; padding: 4px; }

.chat-link-button {
    display: flex;
    gap: 12px;
    align-items: center;
    justify-content: space-between;
    min-width: 220px;
    padding: 10px 12px;
    border: 1px solid var(--focus-accent-border);
    border-radius: var(--pill-radius);
    background: var(--accent-08);
    color: var(--accent-light);
    font-weight: 600;
    text-decoration: none;
}

.chat-link-button:hover { background: var(--accent-18); }

/* The bubble owns the width floor against the definite transcript column:
   260px card plus its 36px bubble padding and 2px border. A child percentage
   alone would lose that floor in a shrink-to-fit bubble, even on desktop. */
.chat-bubble:has(> .chat-quiz-card) {
    min-width: min(298px, 100%);
    margin-inline: 0;
}
/* Owner quiz card (design system: born on tokens; DESIGN.md "Quiz card"). Inside a bubble the bubble is the object, so
   the card adds no box of its own. */
.chat-bubble > .chat-quiz-card { padding: 0; border-color: transparent; background: transparent; gap: var(--space-3); }
.chat-quiz-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
    width: 100%;
    min-width: min(260px, 100%);
    max-width: 560px;
    padding: var(--space-3, 12px);
    border: 1px solid transparent;
    border-radius: var(--radius-object);
    background: var(--surface-object);
}
/* The head is one row in every room: `[Question] ● status … ×`, every item centred on one 28px row. It never
   wraps: on a very narrow card the status takes a second line inside its own slot, so its words stay whole. */
.chat-quiz-head {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    min-height: var(--quiz-row-height);
}
.chat-quiz-chip {
    flex: 0 0 auto;
    padding: 0 8px;
    line-height: 20px;
    white-space: nowrap;
    border-radius: var(--pill-radius);
    font-size: var(--type-meta);
    color: var(--status-neutral-fg);
    background: var(--status-neutral-bg);
}
/* The head's dot is the shared status mark (web/modules/status_mark.js, drawn by ui.css). Its ink is the disabled
   one, which a replaced or closed question wears; every other lifecycle names its own hue. */
.chat-quiz-status {
    --mark-ink: var(--text-disabled);
    display: inline-flex;
    align-items: center;
    gap: var(--space-2, 8px);
    min-width: 0;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    color: var(--text-meta);
}
/* The owner's own record: an answered question's words and its ok dot (ui.css --mark-ok) take the recorded-answer
   roles, the owner's blue on dark, the meta words and ok dot on white. An answer whose delivery is unknown keeps its
   owner dot. */
.chat-quiz-card[data-state="answered"] .chat-quiz-status { color: var(--tone-recorded-answer); --mark-ok: var(--tone-recorded-mark); }
.chat-quiz-question {
    font-size: var(--type-body);
    line-height: var(--line-body, 1.5);
    font-weight: 400;
    color: var(--text-primary);
    min-inline-size: 0;
}
.chat-quiz-question strong { font-weight: 600; }
.chat-quiz-stake {
    font-size: var(--type-meta);
    line-height: var(--line-meta, 1.35);
    color: var(--text-meta);
    min-inline-size: 0;
}
.chat-quiz-stake .inline-code { font-size: inherit; }
/* A question ends on its time alone (owner, refined A 21.10), separate from the card's contents. */
.chat-bubble > .chat-quiz-card + .msg-time { margin-top: var(--space-3); line-height: var(--line-meta); }
/* An offered answer is an elevated card (owner 16.10: Material's elevated card, a choice and never a field): the
   capsule surface on the rest shadow like every pressable thing, the object radius because an answer can be a
   paragraph; hover and press change its colour. Once the question settles the other cards lie flat and the chosen
   one takes the chosen-answer tone (ui.css: the owner's sapphire keeping its lift on dark, the green tone flat on
   white) and names itself `Your choice`. */
.chat-quiz-options {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.chat-quiz-option {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
    padding: 14px 18px;
    border: 0;
    border-radius: var(--radius-object);
    background: var(--surface-capsule);
    box-shadow: var(--elev-rest);
    color: var(--text-primary);
    font-size: var(--type-body);
    font-weight: 600;
    text-align: left;
    cursor: pointer;
    transition: var(--motion-tone);
}
.chat-quiz-option:hover:not(:disabled) { background: var(--surface-capsule-hover); }
.chat-quiz-option:active:not(:disabled) { filter: var(--press); }
.chat-quiz-option:disabled { color: var(--text-disabled); cursor: default; background: var(--surface-tray); box-shadow: none; }
.chat-quiz-option.chosen { color: var(--tone-chosen-answer-ink); background: var(--tone-chosen-answer); }
/* A settled answer is a record: its chosen card keeps the lift the theme gives it. A chosen card that still takes a
   press (a choice dialog's radio card) stands like every capsule. */
.chat-quiz-option.chosen:disabled { box-shadow: var(--tone-chosen-answer-shadow); }
.chat-quiz-option.pending-choice {
    transition: none;
    box-shadow: var(--elev-rest), inset 0 0 0 1px var(--focus-accent-border);
    background: var(--accent-08);
    color: var(--text-primary);
}
.chat-quiz-card [aria-disabled="true"] { cursor: default; }
.chat-quiz-card [aria-disabled="true"]:not(.pending-choice) { opacity: var(--quiz-held-opacity); }
.chat-quiz-card [aria-disabled="true"]:hover:not(.pending-choice) {
    background: var(--surface-tray);
}
.chat-quiz-feedback {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
    color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta);
}
.chat-quiz-feedback[hidden], .chat-quiz-feedback [hidden] { display: none; }
.chat-quiz-submission-note, .chat-quiz-storage-note { flex-basis: 100%; white-space: pre-wrap; }
.chat-quiz-feedback > :is(.chat-quiz-submission-note, .chat-quiz-storage-note):empty { display: none; }
.chat-quiz-storage-note { color: var(--status-warn-fg); }
/* Recovery reads as one row under the answer: Forget (only the local copy) at its start, the server actions at its end. */
.chat-quiz-feedback > .chat-quiz-forget { margin-inline-end: auto; }
.chat-quiz-feedback button:focus-visible {
    outline: 2px solid var(--focus-accent-border); outline-offset: 2px;
}
.chat-quiz-option-detail {
    font-weight: 400;
    font-size: var(--type-meta);
    line-height: var(--line-meta, 1.35);
    color: var(--text-meta);
}
.chat-quiz-option:disabled .chat-quiz-option-detail { color: var(--text-disabled); }
.chat-quiz-option.chosen .chat-quiz-option-detail { color: var(--tone-chosen-answer-detail); }
/* A small word beside an option's label: the asker's recommendation, and on the owner's chosen answer `Your choice`
   (question_presentation.paintChosenAnswer), drawn where the theme shows it (--chosen-answer-cue-display) and in the
   answer's accessible name in every theme. On the chosen card both take its ink. */
.chat-quiz-option-recommended,
.chat-quiz-option-chosen-cue {
    margin-left: var(--space-2, 8px);
    font-size: var(--type-meta);
    font-weight: 600;
}
.chat-quiz-option-recommended { color: var(--tone-recommended); }
.chat-quiz-option-chosen-cue { display: var(--chosen-answer-cue-display); color: var(--tone-chosen-answer-ink); }
.chat-quiz-option.chosen .chat-quiz-option-recommended { color: var(--tone-chosen-answer-ink); }
.chat-quiz-option-chosen-name {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* Free answer: the owner's own words when no offered option fits, in one frame that holds its Send as the composer
   holds its own. The frame wears the field's hairline and, while the words have focus, the field focus idiom; a card
   too narrow for both gives Send its own line inside the frame, at its end. */
.chat-quiz-comment-box {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: flex-end;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-1) var(--space-1) var(--space-3);
    border: 1px solid var(--field-border);
    border-radius: var(--radius-inner);
    background: var(--surface-tray);
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.chat-quiz-comment-box:has(> .chat-quiz-comment:focus) {
    border-color: var(--focus-accent-border);
    box-shadow: 0 0 0 3px var(--focus-accent-ring);
}
/* The words grow to one cap, then scroll (chat_input_sizing.js picks the engine's content sizing, else writes the
   measured height); two lines at rest. */
.chat-quiz-comment {
    flex: 1 1 16em;
    min-width: 0;
    box-sizing: border-box;
    height: var(--chat-input-height, auto);
    min-height: calc(2em * var(--line-body) + 12px);  /* two lines of its own type: a finger's field reads at 16px */
    max-height: var(--message-field-max-height);
    padding: 6px 0;
    border: 0;
    border-radius: 0;
    outline: none;
    background: transparent;
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--type-body);
    line-height: var(--line-body, 1.5);
    resize: none;
}
.chat-quiz-comment[data-sizing="content"] { field-sizing: content; }
.chat-quiz-comment[data-sizing="fit"] { overflow-y: hidden; }
.chat-quiz-comment::placeholder { color: var(--text-meta); }
/* Main's removal of an unknown late answer: a secondary action at its natural width. */
.chat-quiz-card > .chat-quiz-dismiss { align-self: flex-start; }
/* Send is the primary action's crimson capsule at the composer's control height; until there is something to send it
   lies flat. */
.chat-quiz-send {
    flex: none;
    height: var(--composer-control-height);
    padding: 0 var(--space-4);
    border: 0;
    border-radius: var(--pill-radius);
    background: var(--tone-crimson);
    color: var(--tone-crimson-ink);
    font-size: var(--button-font-size);
    font-weight: 600;
    line-height: 1;
    box-shadow: var(--elev-rest);
    cursor: pointer;
    transition: var(--motion-tone);
    --press: var(--press-solid);
}
.chat-quiz-send:hover:not(:disabled) { background: var(--tone-crimson-hover); }
.chat-quiz-send:active:not(:disabled) { filter: var(--press); }
.chat-quiz-send:disabled { color: var(--text-disabled); background: var(--surface-tray); box-shadow: none; cursor: default; }
.chat-quiz-option:focus-visible,
.chat-quiz-send:focus-visible,
.chat-quiz-more:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}
/* The settled record of what the owner answered: a second primary line. */
.chat-quiz-answer {
    white-space: pre-wrap;
    font-size: var(--type-body);
    line-height: var(--line-body, 1.5);
    color: var(--text-primary);
}
.chat-quiz-more {
    align-self: flex-start;
    padding: var(--space-1, 4px) 0;
    border: none;
    background: none;
    color: var(--accent-light);
    font-size: var(--type-meta);
    cursor: pointer;
}
.chat-quiz-assumption {
    font-size: var(--type-meta);
    line-height: var(--line-meta, 1.35);
    color: var(--text-meta);
}

/* Integration-secret request (DESIGN "Quiz card" → Secret request): the card names the asked
   fields and the Settings key each writes, then the host's facts; the masked fields live only in
   the shared modal, in Settings' own secret row. No free answer, no draft. */
.chat-secret-request {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 4px);
    min-inline-size: 0;
}
.chat-secret-fields,
.chat-secret-readers,
.secret-request-readers {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.chat-secret-readers:empty,
.secret-request-readers:empty,
.secret-request-readers-title:empty { display: none; }
.secret-request-readers-title { margin: 0 0 calc(-1 * var(--space-2, 8px)); }
.chat-secret-field {
    display: flex;
    flex-wrap: wrap;
    gap: 0 var(--space-2, 8px);
    align-items: baseline;
    min-inline-size: 0;
}
.chat-secret-field-id {
    font-size: var(--type-body);
    color: var(--text-primary);
    font-weight: 600;
    overflow-wrap: anywhere;
}
.chat-secret-field-key,
.chat-secret-reader,
.secret-request-readers-title,
.secret-request-reader {
    font-size: var(--type-meta);
    line-height: var(--line-meta, 1.35);
    color: var(--text-meta);
    overflow-wrap: anywhere;
}
.chat-secret-status,
.secret-request-late,
.secret-request-warning,
.secret-request-error {
    margin: 0;
    overflow-wrap: anywhere;
}
.chat-secret-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
}
.secret-request-backdrop .secret-request-dialog {
    margin-block: auto;
}
.secret-request-dialog {
    max-width: 480px;
}
/* Keep this reading surface opaque even under the theme's shared floating-sheet rule. */
.marketplace-modal.secret-request-dialog {
    background-color: var(--bg-secondary);
}
.secret-request-body,
.secret-request-fields {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 12px);
}
.secret-request-intro {
    margin: 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta, 1.35);
}
.secret-request-label {
    font-size: var(--type-body);
    color: var(--text-primary);
    font-weight: 600;
    overflow-wrap: anywhere;
}
.secret-request-check { align-self: flex-start; }

/* D12: always-visible compact copy icon in the bubble's bottom-right corner. The Reply control joins it in
   one control row (docs/DESIGN.md "Replies"), and so do the registered message actions between them
   (message_actions.js): the same 24px square, ink and rhythm, one --space-2 gap. */
.chat-message-action,
.chat-message-copy,
.chat-message-reply {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--space-5);
    height: var(--space-5);
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--pill-radius);
    background: transparent;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: 1;
    cursor: pointer;
    opacity: 0.7;
    transition: opacity 0.15s ease, background 0.15s ease;
}
/* On the time's line: the bubble's bottom inset is --space-3, the 24px controls stand --space-2 above its edge. */
.chat-message-copy { position: absolute; right: var(--space-2); bottom: var(--space-2); }
.chat-message-actions {
    position: absolute;
    right: var(--space-2);
    bottom: var(--space-2);
    display: flex;
    align-items: center;
    gap: var(--space-2);
}
/* In the row the copy control is a flex item: the corner offsets above must not move it (relative keeps its hit area). */
.chat-message-actions > .chat-message-copy { position: relative; inset: auto; }
/* Under a finger the 24px Reply and Copy keep their look and gain the shared hit area (ui.css .ui-hit-area); the pair
   parts by the hit area's overshoot so the two targets never overlap. */
@media (pointer: coarse) {
    .chat-message-actions { gap: calc(var(--touch-target-min) - var(--space-5)); }
}

.chat-message-action:hover,
.chat-message-action:focus-visible,
.chat-message-action[aria-pressed="true"],
.chat-message-copy:hover,
.chat-message-copy:focus-visible,
.chat-message-reply:hover,
.chat-message-reply:focus-visible { opacity: 1; }
.chat-message-action:hover,
.chat-message-copy:hover,
.chat-message-reply:hover { background: var(--tone-quiet-hover); }
/* An action that is on now (Read aloud while it speaks) has no body, so it wears the crimson tone's word. */
.chat-message-action[aria-pressed="true"] { color: var(--tone-crimson-word); }
.chat-message-action:focus-visible,
.chat-message-copy:focus-visible,
.chat-message-reply:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

/* Reserve a gutter so the timestamp never sits under the always-visible controls (Copy ends 32px from the
   bubble's edge, Reply and Copy 64px, each registered action one more square and gap; the bubble's own inset is
   --space-4, the gap to the time --space-2). */
.chat-bubble.has-copy .msg-time { margin-right: var(--space-5); }
.chat-bubble.has-reply .msg-time {
    margin-right: calc((var(--space-5) + var(--space-2)) * (2 + var(--chat-message-extra-actions, 0)) - var(--space-2));
}

/* The message a quote points at: a calm ring for a moment, a keyboard ring while it holds focus. */
@keyframes chat-quote-target {
    from { box-shadow: 0 0 0 3px var(--accent-25); }
    to { box-shadow: 0 0 0 3px transparent; }
}
:is(.chat-bubble, .chat-live-card, .chat-live-line).is-quote-target { animation: chat-quote-target 1.4s ease-out; }
/* A row that takes focus only to be returned to (a quote's target, a closed row menu) shows the one keyboard ring. */
:is(.chat-bubble, .chat-live-card, .chat-live-line)[tabindex="-1"]:focus { outline: none; }
:is(.chat-bubble, .chat-live-card, .chat-live-line)[tabindex="-1"]:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
    :is(.chat-bubble, .chat-live-card, .chat-live-line).is-quote-target { animation: none; box-shadow: 0 0 0 3px var(--accent-25); }
}

/* === The quote panel (docs/DESIGN.md "Replies"): one component (chat_reply_quote.js) inside a bubble and
   as the composer field's first line. The bar and the author name wear the QUOTED author's ink, the panel one tint of that same
   ink on the quote glass; no literal colours. A reply shows one ellipsized line and is a button that jumps to
   the quoted message; a relay shows the owner's complete words, clamped to six lines, `Show all` in its head. */
.chat-quote {
    --quote-ink: var(--text-meta);
    --quote-tint: var(--surface-tray);
    position: relative;
    display: block;
    margin: 0 0 var(--space-2);
    border-radius: var(--radius-inner);
    background: linear-gradient(var(--quote-tint), var(--quote-tint)), var(--quote-glass);
    overflow: hidden;
}
.chat-quote[data-author="user"] { --quote-ink: var(--user-sender); --quote-tint: var(--user-bubble-from); }
.chat-quote[data-author="assistant"] { --quote-ink: var(--quote-assistant-ink); --quote-tint: var(--accent-10); }
.chat-quote[data-author="system"] { --quote-ink: var(--system-sender); --quote-tint: var(--system-bubble-bg); }
.chat-quote::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: 2px;
    background: var(--quote-ink);
}
.chat-quote-main {
    display: flex;
    flex-direction: column;
    gap: 0;
    box-sizing: border-box;
    width: 100%;
    min-width: 0;
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border: 0;
    background: transparent;
    color: var(--text-meta);
    font: inherit;
    text-align: start;
    cursor: pointer;
}
.chat-quote-main:hover { background: var(--menu-item-hover); }
/* A quoted photo leads the panel with its thumbnail (the head and the line stand to its right). */
.chat-quote-main.has-thumb {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--space-2);
    align-items: center;
}
.chat-quote-main.has-thumb > .chat-quote-thumb { grid-row: 1 / span 2; }
.chat-quote-thumb {
    display: block;
    width: var(--chat-attach-thumb-size);
    height: var(--chat-attach-thumb-size);
    border-radius: var(--radius-inner);
    object-fit: cover;
    background: var(--surface-tray);
}
.chat-quote-main:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: -2px; }
.chat-quote-head {
    display: flex;
    align-items: baseline;
    gap: var(--space-1);
    min-width: 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    white-space: nowrap;
}
.chat-quote-lead,
.chat-quote-when { flex: none; color: var(--text-meta); }
.chat-quote-author {
    min-width: 0;
    overflow: hidden;
    color: var(--quote-ink);
    font-weight: 600;
    text-overflow: ellipsis;
}
.chat-quote-text {
    display: block;
    min-width: 0;
    overflow: hidden;
    color: var(--text-meta);
    font-size: var(--type-body);
    line-height: var(--line-meta);
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A relay: the owner's words are the instruction, so they read in primary ink, wrapped, up to six lines. */
.chat-quote[data-mode="relay"] { padding: var(--space-2) var(--space-3); }
.chat-quote[data-mode="relay"] > .chat-quote-head { align-items: center; min-height: var(--space-5); }
.chat-quote[data-mode="relay"] > .chat-quote-text {
    margin-top: var(--space-1);
    color: var(--text-primary);
    line-height: var(--line-body);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    text-overflow: clip;
}

/* Notes between works (chat_relay_deck.js; DESIGN "Replies"): a relay row's bubble is the note card. Its head is an
   address (sender → receiver · time); consecutive notes lie as one stack with the newest card in front and up to two
   older cards peeking BELOW it, each 12px narrower per side and fainter, drawn as layers INSIDE the row's own box
   (the row reserves 8px per ledge under the face), so nothing paints above the front card and a contained row
   (content-visibility while the width moves or the row sleeps) clips none of it. The row itself is transparent: the
   face carries the material and the rest shadow, the ledges the cards behind. The rows keep their own identity and
   actions. The owner chose this drawing on 25 Oct 2026 after rejecting edges drawn above the card (24 Oct). */
/* Specific enough to win over the voices' own bubble rules further down (one more class than they carry). */
.chat-bubble.assistant.chat-relay-note {
    --relay-reserve: 0px;
    position: relative;
    isolation: isolate;
    align-self: flex-start;
    width: min(100%, 680px);
    max-width: max(72%, min(100%, 620px));
    margin-right: 0;
    padding: 12px 16px calc(14px + var(--relay-reserve));
    border: 0;
    border-radius: var(--radius-object);
    background: transparent;
    box-shadow: none;
}
.chat-bubble.assistant.chat-relay-note[data-deck-edges="1"] { --relay-reserve: 8px; }
.chat-bubble.assistant.chat-relay-note[data-deck-edges="2"] { --relay-reserve: 16px; }
.chat-bubble.chat-relay-note > :is(.sender, .msg-time) { display: none; }
.chat-bubble.chat-relay-note:not([data-raw="1"]) > :is(.chat-relay-raw-label, .message, .chat-message-actions) { display: none; }
/* A tucked note is a row the deck holds behind its front card: never drawn, whatever else styles the row. */
.chat-bubble.chat-relay-note[data-deck-tucked="1"] { display: none !important; }
/* A fanned column sits a little closer than ordinary messages, so it still reads as one group. */
.chat-relay-note[data-deck="open"]:not([data-deck-first="1"]) { margin-top: -6px; }

/* The face (the card's material on the rest shadow, inset above the ledge reserve) and the ledges behind it. */
.chat-relay-note > :is(.chat-relay-face, .chat-relay-ledge) {
    position: absolute;
    border-radius: var(--radius-object);
    background: var(--voice-ouroboros-note);
    box-shadow: var(--elev-rest);
    pointer-events: none;
}
.chat-relay-note > .chat-relay-face { inset: 0 0 var(--relay-reserve) 0; z-index: -1; transition: var(--motion-tone); }
.chat-relay-note > .chat-relay-ledge {
    display: none;
    left: 12px;
    right: 12px;
    top: 24px;
    bottom: calc(var(--relay-reserve) - 8px);
    z-index: -2;
    background: var(--relay-ledge-1);
    transform-origin: 50% 0;
}
.chat-relay-note > .chat-relay-ledge[data-level="2"] {
    left: 24px;
    right: 24px;
    top: 48px;
    bottom: 0;
    z-index: -3;
    background: var(--relay-ledge-2);
}
.chat-relay-note[data-deck-edges="1"] > .chat-relay-ledge[data-level="1"],
.chat-relay-note[data-deck-edges="2"] > .chat-relay-ledge { display: block; }
/* The closed stack is pressable anywhere on its face: hover deepens the fill a step, a press deepens it again. */
@media (hover: hover) {
    .chat-relay-note[data-deck="closed"] { cursor: pointer; }
    .chat-relay-note[data-deck="closed"]:hover > .chat-relay-face { background: var(--voice-ouroboros-note-hover); }
}
.chat-relay-note[data-deck="closed"]:active > .chat-relay-face:not(:has(button:active)) { filter: var(--press); }
/* The row's own states go to the face, since the row is transparent: a quote's jump rings the card (reduced motion
   keeps the ring), a long press sinks and lifts the row (its transform, below) while the face's shadow dims the feed. */
@keyframes chat-relay-target {
    from { box-shadow: var(--elev-rest), 0 0 0 3px var(--accent-25); }
    to { box-shadow: var(--elev-rest), 0 0 0 3px transparent; }
}
.chat-relay-note.is-quote-target { animation: none; }
.chat-relay-note.is-quote-target > .chat-relay-face { animation: chat-relay-target 1.4s ease-out; }
@media (prefers-reduced-motion: reduce) {
    .chat-relay-note.is-quote-target > .chat-relay-face { animation: none; box-shadow: var(--elev-rest), 0 0 0 3px var(--accent-25); }
}
.chat-bubble.assistant.chat-relay-note[data-row-press] { box-shadow: none; }
.chat-relay-note[data-row-press] > .chat-relay-face { box-shadow: var(--elev-rest), 0 0 0 100vmax transparent; transition: box-shadow 0.24s ease-out; }
.chat-relay-note[data-row-press="lift"] > .chat-relay-face { box-shadow: var(--elev-float), 0 0 0 100vmax var(--scrim); }

/* The address line: who → whom, and at the right edge the time (a range on a closed stack) with the stack's control
   as one unit, so a narrow card wraps that unit whole and never the control alone. */
.chat-relay-card-head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-2);
    min-height: 28px;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.chat-relay-from { min-width: 0; overflow-wrap: anywhere; color: var(--text-primary); font-weight: 600; }
.chat-relay-rooms[hidden] { display: none; }
/* The sender's Project reference at the address line's size (the voice line's 28px, meta); a long name ellipsizes at its cap. */
.chat-relay-card-head > .chat-quiz-project.chat-live-project-card-btn {
    min-height: 28px;
    max-width: min(60%, 360px);
    padding: 0 var(--space-3);
    gap: 6px;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.chat-relay-card-head > .chat-quiz-project .chat-live-project-name { font-weight: 600; }
/* The receiver is half of the address: it keeps its whole name, taking its own line below a long Project capsule
   rather than shrinking; only a name longer than the line stops at its edge. */
.chat-relay-dest { display: inline-flex; align-items: center; gap: var(--space-2); flex: 1 1 96px; min-width: auto; max-width: 100%; }
.chat-relay-arrow { display: inline-flex; flex: none; color: var(--text-meta); }
.chat-relay-arrow > svg { width: 14px; height: 14px; }
.chat-relay-to { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-relay-side { display: inline-flex; align-items: center; gap: var(--space-2); flex: none; margin-inline-start: auto; white-space: nowrap; }
.chat-relay-time { flex: none; font-variant-numeric: tabular-nums; }
/* The stack's one control is the shared small capsule (.btn.btn-default.btn-sm): `N notes` on the closed front card,
   `Show less` on the column's first card. */
.chat-relay-act { flex: none; white-space: nowrap; }
.chat-relay-act[hidden] { display: none; }
.chat-relay-note .chat-relay-more {
    border: 0; background: none; box-shadow: none; padding: 0; min-height: 0; font: inherit; cursor: pointer;
    color: var(--text-primary); font-size: var(--type-meta); font-weight: 600; line-height: var(--line-meta);
}
.chat-relay-note .chat-relay-more:hover { background: none; box-shadow: none; }
.chat-relay-note .chat-relay-more:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }

/* His words lie in tucked cards: the blue mark after the excerpt says so, on the closed front card. */
.chat-relay-words-inside {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-2);
    color: var(--user-sender);
    font-size: var(--type-meta);
    font-weight: 600;
    line-height: var(--line-meta);
}
.chat-relay-words-inside::before { content: ''; flex: none; width: 3px; height: 12px; border-radius: 2px; background: currentColor; }
.chat-relay-excerpt { display: flex; align-items: flex-end; gap: var(--space-2); margin-top: var(--space-2); }
/* The one control at the end of the teller's line; alone, when a note without that line is open, it keeps the right.
   A bare word is 16px tall: with a pointer it gains an invisible 24px hit area centred on it (the touch minimum comes
   from .ui-hit-area under a coarse pointer), so the word itself moves nowhere. */
.chat-relay-more { flex: none; margin-inline-start: auto; }
@media (pointer: fine) {
    .chat-relay-more::after {
        content: "";
        position: absolute;
        inset: min(0px, calc((100% - 24px) / 2));
    }
}
.chat-relay-gist {
    margin: 0;
    flex: 1 1 auto;
    min-width: 0;
    color: var(--assistant-text);
    font-size: var(--type-body);
    line-height: var(--line-body);
    overflow-wrap: anywhere;
    /* Three lines leave the inline more control at the end of the visible text. */
    max-height: calc(3em * var(--line-body));
    overflow: hidden;
}
.chat-relay-note[data-raw="1"] .chat-relay-gist { max-height: none; overflow: visible; }
.chat-relay-note[data-raw="1"] .chat-relay-gist[data-raw="1"] { display: none; }
/* Without the teller's line the note's own first words stand in, in the meta ink: they were written for the work. */
.chat-relay-gist[data-raw="1"] { color: var(--text-meta); }
/* Two lines of his words on a card; the whole quotation once the note is opened. */
.chat-relay-note .chat-clamp { -webkit-line-clamp: 2; line-clamp: 2; }
.chat-relay-note .chat-clamp[data-expanded="1"] { -webkit-line-clamp: unset; line-clamp: unset; }
.chat-relay-note > :is(.chat-quote, .chat-relay-quotes) { margin-top: var(--space-2); }
.chat-relay-raw-label {
    margin-top: var(--space-3);
    color: var(--text-meta);
    font-size: var(--type-meta);
    font-weight: 600;
    line-height: var(--line-meta);
}
.chat-relay-note[data-raw="1"] > .message {
    margin-top: 2px;
    padding: 10px 12px;
    border-radius: var(--radius-inner);
    background: var(--surface-tray);
    color: var(--text-secondary);
    font-size: var(--type-body);
    line-height: var(--line-body);
}
:root[data-theme="light"] .chat-relay-note[data-raw="1"] > .message { background: rgba(var(--shell-rgb), 0.72); }

/* The clamp of a long quotation (a relay panel's text); its toggle lives in the panel's head, a row that already
   exists, so no control floats on a row of its own. */
.chat-clamp {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 6;
    line-clamp: 6;
    overflow: hidden;
}
.chat-clamp[data-expanded="1"] { display: block; -webkit-line-clamp: unset; line-clamp: unset; overflow: visible; }
.chat-clamp-toggle { flex: none; margin-inline-start: auto; }
/* The files that came with the owner's relayed words: named, not served again. */
.chat-quote-files {
    margin-top: var(--space-1);
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}
.chat-clamp-toggle[hidden] { display: none; }

/* A staged reply is the field's first line: the same panel inside the field, concentric with its corner (the sheet
   radius less the field's 8 px inset), with its cancel control. No hairline of its own: the field is the object. */
.chat-field > .chat-quote--composer {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    /* Its own bottom margin takes the text row 4 px closer (the row keeps its padding), so all the room the strip
       takes is its own box: its fold takes it back whole. It lies over the row's padding, its controls whole. */
    margin: var(--space-2) var(--space-2) calc(var(--space-1) - var(--space-2));
    z-index: 1;
    padding-inline-end: var(--space-2);
    border-radius: calc(var(--radius-sheet) - var(--space-2));
    /* The quote's tint on the field itself: the field is already the glass, and fills never stack. */
    background: var(--quote-tint);
}
/* It opens and closes like the capsule row's fold (owner 20.10, Q6): its height from 0 on the sheet curve while its
   words fade, and its insets taken back while it is shut, so the field grows and shrinks with it (chat_reply.js
   measures --reply-h and the room the strip takes at rest, --reply-room). */
.chat-field > .chat-quote--composer[data-motion] {
    overflow: hidden;
    max-height: var(--reply-h);
    transition: max-height var(--motion-close) var(--ease-sheet), margin-top var(--motion-close) var(--ease-sheet),
        margin-bottom var(--motion-close) var(--ease-sheet), opacity var(--motion-fade) ease;
}
.chat-field > .chat-quote--composer:is([data-motion="shut"], [data-hold]) {
    max-height: 0;
    margin-top: 0;
    margin-bottom: 0;
    opacity: 0;
}
/* Held through the first frame after it was staged (chat_reply.js): shut at once, then it opens from the next. */
.chat-field > .chat-quote--composer[data-hold] { transition: none; }
/* The sent draft's copy in flight (send_flight.js; DESIGN "The composer field"): the new bubble's own look, fixed over
   the room for one motion, above a Project and below the drawer, inert. */
.chat-bubble.chat-send-flight {
    position: fixed;
    z-index: var(--z-send-flight);
    box-sizing: border-box;
    margin: 0;
    max-width: none;
    overflow: hidden;
    pointer-events: none;
}
.chat-quote--composer .chat-quote-main { flex: 1 1 auto; width: auto; padding-block: var(--space-2); padding-inline-start: var(--space-5); }
/* In the rounded strip the author bar stands inside the curve, straight from end to end (the bubble quote's
   bar meets square-ish corners; here the pill would bend it into a crescent). */
.chat-quote--composer::before {
    inset-block: var(--space-2);
    inset-inline-start: var(--space-3);
    border-radius: var(--pill-radius);
}
.chat-quote-cancel {
    flex: none;
    display: grid;
    place-items: center;
    width: var(--composer-control-height, 32px);
    height: var(--composer-control-height, 32px);
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--text-meta);
    cursor: pointer;
}
.chat-quote-cancel:hover { background: var(--menu-item-hover); color: var(--text-primary); }
.chat-quote-cancel:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: -2px; }
@media (prefers-reduced-motion: reduce) {
    .chat-field > .chat-quote--composer[data-motion] { transition: none; }
}
/* A finger needs the shared touch target on the controls these blocks add: the doubt's actions and a question's
   recovery row wrap on a phone, so their buttons take the minimum as their own height through the control tokens
   (an invisible reach would cover a neighbour; audit №4, owner 2026-10-28), while the clamp toggle keeps its look and
   its reach; the time under the doubt's actions then keeps clear of the Reply / Copy targets that sit on its line. */
@media (pointer: coarse) {
    :is(.msg-pending, .chat-quiz-feedback) { --control-h: var(--touch-target-min); --control-h-sm: var(--touch-target-min); }
    .chat-bubble:is(.has-reply, .has-copy) > .msg-pending:has(.btn):has(+ .msg-time) { margin-bottom: var(--space-3); }
}

.chat-copy-fallback {
    position: fixed;
    left: -10000px;
    top: 0;
    width: 1px;
    height: 1px;
    opacity: 0;
}

@media (max-width: 620px) {
    .chat-bubble.is-multiple .chat-gallery-grid { grid-template-columns: 1fr; }
    .chat-media-controls { flex-wrap: wrap; }
    .chat-media-progress { order: 2; flex-basis: 100%; }
    .chat-media-rate-menu { display: none; }
}


/* Compact horizontal spacing leaves room for wider native system-font metrics. */
.chat-panel-statusbar { flex-wrap: wrap; gap: var(--space-2) var(--space-1); }
.chat-live-executor-chip { max-width: 100%; white-space: normal; }
.chat-live-meta-text, .chat-live-executor-chip .harness-identity-label { overflow-wrap: anywhere; }
/* A reading measure for the owner's bubbles; the left voice's prose takes the feed's one measure (--feed-measure,
   ~75 characters at its widest), so a long answer ends on the work cards' edge (U009). */
.chat-bubble:not(.assistant, .system) .message:not(:has(> *)),
.chat-bubble:not(.assistant, .system) .message > :is(p:not(:has(img, video)), ul, ol, blockquote, .md-h1, .md-h2, .md-h3) {
    max-inline-size: 72ch;
}

/* Light surfaces keep geometry and domain state semantics unchanged. */
:root[data-theme="light"] #nav-version { opacity: 1; color: var(--text-meta); }
:root[data-theme="light"] .files-preview-content { color: var(--text-primary); }
:root[data-theme="light"] .files-preview-frame { background: var(--bg-secondary); }
:root[data-theme="light"] .skill-review-summary-main { color: var(--system-text); }
:root[data-theme="light"] .skill-review-summary-side { color: var(--text-meta); }
:root[data-theme="light"] .chat-bubble .message pre { background: var(--code-bg); }
:root[data-theme="light"] .md-code-block .hljs-comment,
:root[data-theme="light"] .md-code-block .hljs-quote { color: var(--text-meta); }
:root[data-theme="light"] .md-code-block .hljs-keyword,
:root[data-theme="light"] .md-code-block .hljs-type,
:root[data-theme="light"] .md-code-block .hljs-selector-tag,
:root[data-theme="light"] .md-code-block .hljs-literal { color: var(--project); }
:root[data-theme="light"] .md-code-block .hljs-string,
:root[data-theme="light"] .md-code-block .hljs-regexp,
:root[data-theme="light"] .md-code-block .hljs-addition,
:root[data-theme="light"] .md-code-block .hljs-template-variable { color: var(--status-ok-fg); }
:root[data-theme="light"] .md-code-block .hljs-number,
:root[data-theme="light"] .md-code-block .hljs-built_in { color: var(--accent-light); }
:root[data-theme="light"] .md-code-block .hljs-title,
:root[data-theme="light"] .md-code-block .hljs-attr,
:root[data-theme="light"] .md-code-block .hljs-variable,
:root[data-theme="light"] .md-code-block .hljs-function { color: var(--user-sender); }
:root[data-theme="light"] .md-code-block .hljs-class .hljs-title { color: var(--status-warn-fg); }
:root[data-theme="light"] .status-badge.online { color: var(--status-ok-fg); }
:root[data-theme="light"] .status-badge.error { color: var(--status-error-fg); }
