Files
fusion/packages/dashboard/app/components/PlanningModeModal.css
gsxdsm cdf67c1d98 fix(dashboard): stop Planning Mode retry loop, make AI sessions multi-tab (#2101)
## Problem

Reported: planning gets stuck in a cycle of retrying and regenerating
after a response was already supplied.

After the user answers a planning question, `submitResponse` pushed the
answer to history but left `session.currentQuestion` pointing at the
just-answered question for the whole next generation. The planning SSE
route's catch-up path re-emits `currentQuestion` to every fresh
connection — and each FN-7946 auto-retry (#2073) opens a fresh
connection. So after any generation error:

1. Auto-retry connects a fresh stream → the server re-emits the
**already-answered** question.
2. The client treats any question event as progress: it **resets the
3-attempt auto-retry budget** and re-shows the answered question.
3. The retry regenerates; if it errors again the cycle repeats with a
fresh budget — an unbounded retry/regenerate loop. Re-answering the
stale question also 409-collided with the in-flight generation, feeding
the same loop.

## Fix

Invariant: `currentQuestion` is only set while the session is genuinely
awaiting user input.

- `submitResponse` clears it the moment an answer is accepted (normal
turns and the deepening checkpoint), while preserving the legacy 200
respond contract on generation failure (the modal ignores the body and
lets the SSE error drive recovery).
- `retrySession` scrubs stale questions persisted by pre-fix builds
before regenerating.
- `buildSessionFromRow` only restores a question when the persisted row
is `awaiting_input`.
- `didSubmitSameAnswer` now compares against the last history entry so
the duplicate-submit 409 message survives.
- Agent onboarding gets the same fix (its SSE route also re-emits
`currentQuestion` on connect); retry now asks the next question instead
of re-asking the answered one.

Surface enumeration: mission and milestone interviews keep questions the
same way but their SSE routes never re-emit on connect, and the
auto-retry budget machinery is Planning-Mode-only — planning +
onboarding were the two affected surfaces.

## Symptom Verification

- **Original symptom:** after answering a question, Planning Mode loops
between "Retrying…" and regenerating, re-showing the already-answered
question, with the auto-retry budget never exhausting.
- **Exact reproduction:** answer a question, have the next generation
fail (stuck watchdog/provider error), let the client auto-retry open a
fresh SSE connection.
- **Assertion it is gone:** new regression suite
`planning-answered-question-reemit.test.ts` asserts `currentQuestion` is
cleared mid-generation, on generation failure, on retry, and on restore
from non-`awaiting_input` rows — so the SSE catch-up path has nothing
stale to re-emit. All 5 tests fail against pre-fix code and pass with
the fix; an onboarding regression test covers the sibling surface.

## Verification

- New regression tests: 5/5 fail on pre-fix code, pass with the fix
(plus 1 onboarding test).
- Existing suites: 137 planning server tests pass (3 failures in
`routes-planning.test.ts` fail identically without this change —
pre-existing on the branch); all 69 `PlanningModeModal.planning-flow`
client tests pass; `tsc --noEmit` clean; `pnpm check:changesets` passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Made Planning Mode (and related planning controls) lock-free and
multi-tab—no more take-over/active-in-another-tab lock overlays.

* **Bug Fixes**
* Fixed Planning Mode retry/generation flows where already-answered
questions could reappear.
* Ensured answered questions clear immediately and aren’t re-emitted
during session recovery/SSE catch-up.
* Improved session restoration and preserved legacy recovery behavior
when generation fails after an answer.

* **Tests**
* Added regression coverage for the answered-question invariant and
updated existing tests to reflect lock-free behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---

## Follow-up: Planning Mode is now multi-tab via DB state (lock-free)

Second commit removes all cross-tab coordination from planning — the
persisted session row is the single source of truth and multiple tabs
can read and interact with the same session:

- **Server:** `/planning/*` routes no longer run `checkSessionLock` or
parse `tabId`; a stale `tabId` from an older client is ignored instead
of 409'd. Subtask/mission interview routes keep their existing lock
behavior.
- **Client:** `PlanningModeModal` drops `useSessionLock`, the
`useAiSessionSync` BroadcastChannel broadcasts,
`sessionTabId`/`lockSessionId` state, and the "Take Control" overlay.
Tabs stay current via the per-session SSE stream plus the global
`ai_session:updated` events `useBackgroundSessions` already consumes;
concurrent writes resolve via the server's generation-in-progress guard
(409).
- **API client:** planning functions lose their `tabId` params.
- **Fix uncovered by the refactor:** the 8s stuck-poll now resolves the
session id inside each tick — the removed lock state was what previously
re-armed the poll after Start Planning resolved the session id.
- Also fixes a pre-existing PG-cutover break in
`planning-generation-cancellation.test.ts` (`getSession` is async).

Verification: 144 client planning tests and 137 server planning tests
pass (the 3 remaining `routes-planning.test.ts` failures are
pre-existing on the branch and fail identically without these changes);
`tsc --noEmit` and eslint clean on changed files; `pnpm
check:changesets` passes. Lock-conflict route tests were rewritten to
assert lock-free semantics, plus a new modal test proving a session
stays fully interactive with no lock acquisition even when another tab
is active.


---

## Follow-up 2: the per-tab session lock is gone entirely

Third commit extends the multi-tab model from planning to **every** AI
interview surface (planning, subtask breakdown, mission interview,
milestone/slice interview) and deletes the lock machinery root and
branch.

**Server**
- Deleted the `/ai-sessions/:id/lock`, `/lock/force`, and `/lock/beacon`
routes.
- Dropped `checkSessionLock` from every
planning/subtask/mission/milestone route (both copies — `routes.ts` and
`mission-routes.ts`). A `tabId` from an older client is ignored, never
409'd; all `tabId` body parsing is gone.
- Dropped `acquireLock` / `releaseLock` / `forceAcquireLock` /
`getLockHolder` / `releaseStaleLocks` from `AiSessionStore`, plus the
`@fusion/core` async helpers (`acquireAiSessionLock` et al) and core's
re-exports.
- Removed `lockedByTab`/`lockedAt` from
`AiSessionRow`/`AiSessionSummary`, the upsert SQL, and all four session
producers.

**Client**
- Deleted `useSessionLock` and the now-orphaned `getSessionTabId` util.
- Removed the Take Control overlay, the "active in another tab" banners,
and `BackgroundTasksIndicator`'s active-elsewhere gate (the confirm
prompt and lock badge — sessions now just open).
- Reduced `useAiSessionSync` to what its own comments already called it
— a low-latency *status* supplement to SSE: no `activeTabMap`,
`broadcastLock/Unlock/Heartbeat`, `owningTabId`, `tab:*` messages, or
stale-heartbeat sweep.
- Dropped `tabId` from every session API client function; removed the
lock CSS.

**Deliberately kept: the two DB columns.** `ai_sessions.locked_by_tab` /
`locked_at` remain as dead, always-NULL columns with a deprecation note.
Dropping them is an irreversible migration, and released binaries still
name those columns explicitly in their upsert — an older install pointed
at the same database would fail every session write. They can be dropped
once no such binary can reach it. No code reads or writes them.

**Verification**: 397 client tests and 137 server planning tests pass
(the same 3 `routes-planning.test.ts` failures are pre-existing —
verified identical on a clean stash); `tsc --noEmit` clean for
`@fusion/core` and `@fusion/dashboard`; eslint clean on all changed
files; the 30 PG `schema-applier` tests pass (they exercise the retained
columns); `pnpm check:changesets` passes. The lock-conflict route tests
and both modal lock tests were rewritten to assert the inverse: routes
and modals stay fully interactive while another tab "holds" a lock, and
the lock API is never called.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 18:47:53 -07:00

1847 lines
44 KiB
CSS

/* === Planning Mode Styles === */
.planning-modal-overlay {
display: flex;
align-items: center;
justify-content: center;
}
.planning-modal *,
.planning-modal {
scrollbar-color: var(--border) transparent;
scrollbar-width: thin;
}
.planning-modal *::-webkit-scrollbar,
.planning-modal::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.planning-modal *::-webkit-scrollbar-track,
.planning-modal::-webkit-scrollbar-track {
background: transparent;
}
.planning-modal *::-webkit-scrollbar-thumb,
.planning-modal::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: var(--radius-sm);
}
.planning-modal *::-webkit-scrollbar-thumb:hover,
.planning-modal::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
.planning-modal *::-webkit-scrollbar-corner,
.planning-modal::-webkit-scrollbar-corner {
background: transparent;
}
.planning-modal {
width: min(95vw, 1200px);
max-width: 95vw;
min-width: 360px;
height: 85vh;
min-height: 480px;
max-height: calc(100dvh - var(--overlay-padding-top, 10vh) - 16px);
overflow: hidden;
resize: both;
}
/*
FNXC:PlanningMode 2026-06-21-00:00:
FN-6886 promotes Planning Mode into the main app content area. The embedded shell fills the available content pane and intentionally disables modal-only sizing/resizing so the left sidebar owns navigation and no backdrop shell remains.
*/
/*
FNXC:Planning 2026-06-23-02:00:
The embedded Planning wrapper matches Missions' shell: NO outer padding. Missions' inline shell (mission-manager--inline) has no wrapper inset — its header supplies the top/side padding (var(--space-lg) var(--space-xl)) and the split body fills edge-to-edge below. Mirroring that here removes the previous extra var(--space-lg) inset so Planning's header and two-pane body align with Missions.
*/
.planning-view {
height: 100%;
min-height: 0;
width: 100%;
flex: 1;
display: flex;
overflow: hidden;
}
/*
FNXC:PlanningMode 2026-06-22-15:30:
FN-6886 full-view fix: embedded planning must fill the entire main-content pane with no awkward gaps. The flex/height chain is planning-view (flex:1, height:100%) -> planning-modal--embedded (flex:1, height:100%) -> planning-modal-body (flex:1, min-height:0). flex:1 + min-width:0 + min-height:0 on the embedded panel lets it consume all remaining width/height inside the flex .planning-view wrapper instead of collapsing to the base .modal 480px width.
*/
.planning-modal--embedded {
flex: 1;
width: 100%;
max-width: none;
min-width: 0;
height: 100%;
min-height: 0;
max-height: none;
resize: none;
box-shadow: none;
}
/*
FNXC:PlanningMode 2026-06-22-00:00:
Embedded planning must blend into the main content like Command Center: no panel shadow, no border/outline, no rounded card chrome, and a transparent background so the view sits flush on the project-content surface. Higher specificity (.planning-view .planning-modal--embedded) is required to beat the base .modal shadow/border/background.
*/
.planning-view .planning-modal--embedded {
box-shadow: none;
border: none;
border-radius: 0;
background: transparent;
/*
FNXC:Planning 2026-06-23-04:45:
The base .modal sets max-height:80vh; that wins over the equal-specificity .planning-modal--embedded { max-height:none } by source order, so the embedded panel was capped at 80vh (≈576px) and stopped ~49px short of the .planning-view bottom (measured: embedded bottom 635 vs view bottom 684). Re-assert max-height:none here at higher specificity (.planning-view .planning-modal--embedded) so the embedded shell — and therefore the sidebar list+footer and the detail pane below it — fills the full main-content height with no bottom gap, in both the initial (no-session) and active-session states.
*/
max-height: none;
}
/*
FNXC:Planning 2026-06-23-03:00:
The embedded planning header must be a visual SIBLING of MissionManager's inline header (mission-manager__header--inline). Matched values, copied exactly from MissionManager.css:
- background: var(--surface), padding: var(--space-lg) var(--space-xl) (= 16px 24px)
- no bottom divider — app headers are seamless against their content while preserving shared surface background and spacing.
- title: 1.125rem / 600 / var(--text) via the h3 rule below (font-weight/color come from the base .modal-header h3).
- icon: var(--todo) tint + flex-shrink:0, identical to .mission-manager__header-icon (the shared icon-triage tint was a different brown, so it is overridden here).
- gap icon<->title: var(--space-sm) (= 8px), identical to .mission-manager__header-title. Scoped to the embedded planning header so the shared .detail-title-row (TaskDetailModal) keeps its own 10px gap.
*/
/* FNXC:ViewHeader 2026-06-23-04:15: Pin canonical --view-header-min-height (≈61px border-box) + box-sizing + vertical centering so embedded Planning's header matches the shared ViewHeader/Missions height exactly. */
.planning-modal--embedded .modal-header--embedded {
box-sizing: border-box;
align-items: center;
min-height: var(--view-header-min-height);
padding: var(--space-lg) var(--space-xl);
background: var(--surface);
border-bottom: none;
}
.planning-modal--embedded .modal-header--embedded .detail-title-row {
gap: var(--space-sm);
}
.planning-modal--embedded .modal-header--embedded .detail-title-row > svg {
color: var(--todo);
flex-shrink: 0;
}
.planning-modal--embedded .modal-header--embedded h3 {
font-size: 1.125rem;
font-weight: 600;
color: var(--text);
letter-spacing: normal;
}
.planning-modal .modal-header {
flex-shrink: 0;
}
.planning-modal .text-muted {
color: var(--text-muted);
}
.planning-modal-body {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
.planning-modal-body--split {
flex-direction: row;
}
.planning-detail {
flex: 1;
min-width: 0;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
/*
FNXC:Planning 2026-06-23-01:15:
Embedded Planning is a real two-pane view mirroring Missions (mission-manager__split): the left sidebar is a full-height flex column (list scrolls, footer pinned to bottom) and the right detail pane fills the remaining width AND height. The sidebar surface/border/width treatment matches mission-manager__sidebar — var(--surface) background, a single border-right divider — so the two views read consistently. The flex chain planning-view -> planning-modal--embedded -> planning-modal-body keeps the whole layout filling the main-content pane to the bottom.
*/
.planning-sidebar {
width: calc(var(--space-lg) * 18.75);
flex-shrink: 0;
border-right: var(--btn-border-width) solid var(--border);
background: var(--surface);
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
/*
FNXC:Planning 2026-06-23-02:00:
Sidebar resize handle — identical treatment to MissionManager's mission-manager__sidebar-resize-handle: a thin (var(--space-sm)) col-resize strip whose ::before center bar tints with var(--todo) on hover/active, plus a focus ring for keyboard resize. touch-action:none keeps the pointer drag from scrolling on trackpads.
*/
.planning-sidebar-resize-handle {
position: relative;
width: var(--space-sm);
flex-shrink: 0;
cursor: col-resize;
background: transparent;
touch-action: none;
transition: background var(--transition-fast);
}
.planning-sidebar-resize-handle::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: var(--space-xs);
transform: translateX(-50%);
}
.planning-sidebar-resize-handle:hover::before,
.planning-sidebar-resize-handle:active::before {
background: color-mix(in srgb, var(--todo) 30%, transparent);
}
.planning-sidebar-resize-handle:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
/*
FNXC:Planning 2026-06-23-02:00:
Footer matches MissionManager's mission-manager__sidebar-footer EXACTLY: padding var(--space-md), a single border-top divider (var(--btn-border-width) solid var(--border)), gap var(--space-sm), centered. Missions has no separate footer band styling beyond the centered CTA, so we mirror that — no extra bottom padding/margin and no empty space. The column direction is the only deviation, required because Planning carries the show/hide-archived link beneath the CTA (Missions has a single child); align-items stays stretch so the full-width CTA fills the footer like Missions'.
*/
.planning-sidebar-footer {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
gap: var(--space-sm);
padding: var(--space-md);
border-top: var(--btn-border-width) solid var(--border);
flex-shrink: 0;
}
.planning-sidebar-toggle-archived-link {
align-self: center;
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
color: var(--text-muted);
text-decoration: none;
cursor: pointer;
transition: color var(--transition-fast), box-shadow var(--transition-fast);
border-radius: var(--radius-sm);
}
.planning-sidebar-toggle-archived-link:hover {
color: var(--text);
text-decoration: underline;
}
.planning-sidebar-toggle-archived-link:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
/*
FNXC:Planning 2026-06-23-02:00:
The New session button must look EXACTLY like Missions' primary sidebar create button (mission-manager__sidebar-cta with btn btn-primary): same full-width fill, same min-height (calc(--space-lg * 2 + --space-xs)), centered icon+label gap, inherited font-size. The earlier .active "inset ring" outline is REMOVED — Missions' CTA has no ring in any state, so the no-session-selected (.active) state now relies solely on the empty/initial detail pane to signal the new-session view, keeping the two buttons visually identical.
*/
.planning-sidebar-new {
width: 100%;
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
justify-content: center;
gap: var(--space-sm);
font-size: inherit;
}
.planning-sidebar-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: var(--space-sm);
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.planning-sidebar-empty {
padding: var(--space-lg) var(--space-md);
font-size: calc(var(--space-sm) + var(--space-xs));
text-align: center;
line-height: 1.5;
}
.planning-sidebar-item {
position: relative;
display: flex;
align-items: stretch;
border-radius: var(--radius-md);
transition: background var(--transition-fast);
}
.planning-sidebar-item:hover {
background: var(--card-hover);
}
.planning-sidebar-item.selected {
background: color-mix(in srgb, var(--todo) 18%, transparent);
}
.planning-sidebar-item.pending-delete {
background: color-mix(in srgb, var(--color-error) 15%, transparent);
}
.planning-sidebar-item-button {
flex: 1;
min-width: 0;
display: flex;
align-items: flex-start;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-sm) var(--space-sm) var(--space-md);
background: none;
border: none;
color: inherit;
text-align: left;
cursor: pointer;
font: inherit;
border-radius: var(--radius-md);
}
.planning-sidebar-item-button:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.planning-sidebar-item-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.planning-sidebar-item-title {
font-size: 13px;
font-weight: 500;
color: var(--text);
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.planning-sidebar-item-meta {
font-size: 11px;
color: var(--text-muted);
display: flex;
gap: 4px;
align-items: center;
}
.planning-sidebar-status-icon {
flex-shrink: 0;
margin-top: 2px;
}
.planning-sidebar-status-generating { color: var(--todo); }
.planning-sidebar-status-awaiting { color: var(--triage); }
.planning-sidebar-status-complete { color: var(--color-success); }
.planning-sidebar-status-error { color: var(--color-error); }
.planning-sidebar-item-actions {
display: flex;
align-items: stretch;
}
.planning-sidebar-item-delete,
.planning-sidebar-item-archive {
flex-shrink: 0;
width: calc(var(--space-lg) + var(--space-xl));
min-width: calc(var(--space-lg) + var(--space-xl));
height: calc(var(--space-lg) + var(--space-xl));
min-height: calc(var(--space-lg) + var(--space-xl));
display: none;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: color var(--transition-fast), background var(--transition-fast), box-shadow var(--transition-fast);
}
.planning-sidebar-item:hover .planning-sidebar-item-delete,
.planning-sidebar-item:focus-within .planning-sidebar-item-delete,
.planning-sidebar-item:hover .planning-sidebar-item-archive,
.planning-sidebar-item:focus-within .planning-sidebar-item-archive {
display: flex;
}
.planning-sidebar-item-delete:hover {
color: var(--color-error);
background: color-mix(in srgb, var(--color-error) 15%, transparent);
}
.planning-sidebar-item-archive:hover {
color: var(--todo);
background: color-mix(in srgb, var(--todo) 15%, transparent);
}
.planning-sidebar-item-delete:focus-visible,
.planning-sidebar-item-archive:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.planning-sidebar-item.archived .planning-sidebar-item-button {
opacity: 0.6;
}
.planning-sidebar-confirm {
display: flex;
align-items: center;
gap: 4px;
padding: 0 8px;
}
.planning-mobile-back {
/* Visible only on mobile (see media query below). On desktop the sidebar
is always visible alongside the detail pane, so a back button is never
needed. */
display: none;
background: none;
border: none;
color: var(--text);
cursor: pointer;
padding: var(--space-xs);
border-radius: var(--radius-sm);
transition: background var(--transition-fast), box-shadow var(--transition-fast);
}
.planning-mobile-back:hover {
background: var(--card-hover);
}
.planning-mobile-back:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
/* Mobile: stack — only one pane visible at a time.
FNXC:PlanningMode 2026-06-22-15:30 covers both the landscape phone case
(max-height: 480px exceeds 768px wide) and portrait. */
@media (max-width: 768px), (max-height: 480px) {
/* Full-screen sheet — drop overlay padding so the modal fills the
viewport instead of being pushed below it, and disable resize since
touchscreen users can't drag the corner grip anyway.
FNXC:PlanningMode 2026-06-22-15:30: scope the viewport-takeover rules to
the NON-embedded (dialog) presentation only. The embedded panel
(.planning-modal--embedded) must NOT grab 100vw/100dvh — it lives inside
the main-content pane, so forcing full-viewport sizing made it overflow
the content area on mobile. The :not(.planning-modal--embedded) guard
keeps the modal full-screen sheet intact while letting the embedded view
fill only its own pane (handled by the .planning-view rules below). */
.modal-overlay:has(.planning-modal) {
padding-top: 0;
align-items: stretch;
justify-content: stretch;
}
.modal.planning-modal:not(.planning-modal--embedded) {
width: 100vw;
min-width: 0;
max-width: 100vw;
height: 100dvh;
min-height: 0;
max-height: 100dvh;
margin: 0;
border: none;
border-radius: 0;
resize: none;
}
.modal.planning-modal:not(.planning-modal--embedded)[style*="--keyboard-overlap"] {
height: var(--vv-height, 100dvh);
max-height: var(--vv-height, 100dvh);
transform: translateY(var(--vv-offset-top, 0px));
will-change: transform;
}
/* FNXC:PlanningMode 2026-06-22-15:30: on mobile the embedded view should use
the full width of the (already-narrow) content pane — drop the outer
padding so the session list and detail panes are edge-to-edge, and keep
the height/flex chain filling the pane. */
.planning-view {
padding: var(--space-sm);
}
/*
FNXC:PlanningMode 2026-06-25-13:10:
The global mobile modal rule in styles.css (`@media (max-width:768px) .modal:not(.confirm-dialog), .modal-lg, ...`) forces every full-screen modal to height/max-height:100dvh. The embedded Planning shell carries `.modal-lg`, so it matched that rule and was stretched to the full 100dvh viewport even though its parent `.planning-view` is bounded between the header and the bottom nav (≈688px). Because `.modal:not(.confirm-dialog)` resolves to specificity (0,2,0) — equal to `.planning-view .planning-modal--embedded` — the global rule won on source order, so the embedded shell overflowed past the viewport, `.planning-view { overflow:hidden }` clipped the footer action buttons, and the summary/question panes could not scroll on mobile. Qualify with `.planning-view.open` (0,3,0) and re-pin max-height so the embedded view stays inside its bounded pane and the inner flex scroll chain works.
*/
.planning-view.open .planning-modal--embedded {
width: 100%;
height: 100%;
max-height: 100%;
}
.planning-modal-body--split {
flex-direction: column;
}
.planning-sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border);
}
/*
FNXC:PlanningMode 2026-06-30-00:00:
Mobile list view must keep the New session CTA reachable like Missions' bottom sidebar action. Bound the sidebar to the Planning pane and let only the session list scroll above the fixed footer; detail view still hides the sidebar through the existing show-detail rule.
*/
.planning-modal-body--show-list {
flex: 1;
min-height: 0;
overflow: hidden;
}
.planning-modal-body--show-list .planning-sidebar {
flex: 1 1 auto;
height: 100%;
min-height: 0;
max-height: 100%;
}
.planning-modal-body--show-list .planning-sidebar-list {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
.planning-modal-body--show-list .planning-sidebar-footer {
flex-shrink: 0;
}
.planning-modal-body--show-detail .planning-sidebar {
display: none;
}
.planning-modal-body--show-list .planning-detail {
display: none;
}
/* Show back button on mobile whenever it's rendered (the React component
only renders it when `mobileShowDetail` is true). */
.planning-mobile-back {
display: inline-flex;
}
/* Always keep action buttons visible on mobile (no hover) */
.planning-sidebar-item-delete,
.planning-sidebar-item-archive {
display: flex;
}
}
.planning-error {
flex-shrink: 0;
margin: 24px 24px 0;
}
.planning-view-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--space-xl);
padding: var(--space-xl);
}
/*
FNXC:Planning 2026-06-23-04:00:
The footer is a light action row, NOT a heavy bordered band. Drop the border-top divider and trim the padding so the Start-Planning button flows with the content above instead of reading as a large bottom footer.
*/
.planning-view-footer {
display: flex;
justify-content: center;
padding: var(--space-sm) var(--space-xl) var(--space-md);
flex-shrink: 0;
}
/*
FNXC:Planning 2026-06-23-03:00:
An empty footer must NOT reserve vertical space or paint its divider band. When .planning-view-footer renders with no children (e.g. the initial / no-active-session state once its action is absent), collapse it to zero so the embedded Planning view shows no dead footer band. Footers WITH content (the Start-Planning button in initial, and the .planning-actions composer in an active session) are untouched — :empty only matches when there are no element/text children.
*/
.planning-view-footer:empty {
display: none;
padding: 0;
border-top: none;
}
.planning-advanced-disclosure {
width: 100%;
max-width: 520px;
margin: 0 auto;
}
.planning-advanced-disclosure .onboarding-disclosure-content {
padding-inline-start: 0;
display: flex;
justify-content: center;
}
.planning-advanced-content {
display: flex;
flex-direction: column;
gap: var(--space-md);
width: 100%;
}
.planning-advanced-section {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-md);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.planning-advanced-blurb {
margin: 0;
color: var(--text-muted);
font-size: calc(var(--space-sm) + var(--space-xs));
line-height: 1.4;
}
.planning-model-select-group {
max-width: 100%;
margin: 0;
width: 100%;
}
.planning-model-select-group .form-label {
font-size: calc(var(--space-sm) + var(--space-xs) + var(--space-xs) * 0.25);
color: var(--text-muted);
text-align: left;
}
.planning-depth-selector {
align-items: stretch;
justify-content: flex-start;
max-width: 100%;
margin: 0;
width: 100%;
}
.planning-depth-controls-row {
display: flex;
align-items: flex-end;
gap: var(--space-md);
flex-wrap: wrap;
}
.planning-depth-chip-group {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
}
.planning-depth-chip.btn {
border-radius: var(--radius-pill);
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
min-height: calc(var(--space-lg) + var(--space-xl));
padding: var(--space-sm) var(--space-md);
}
.planning-depth-chip.btn:hover {
border-color: var(--todo);
background: var(--card-hover);
}
.planning-depth-chip-active.btn,
.planning-depth-chip-active.btn:hover {
background: var(--todo);
border-color: var(--todo);
color: var(--bg);
}
.planning-depth-question-count {
display: flex;
flex-direction: column;
gap: var(--space-xs);
align-items: flex-start;
font-size: 12px;
color: var(--text-muted);
}
.planning-depth-question-input.input {
width: calc(var(--space-2xl) * 2 + var(--space-xs));
min-height: calc(var(--space-lg) + var(--space-xl));
padding: var(--space-xs) var(--space-sm);
}
/* Initial View */
.planning-initial {
display: flex;
flex-direction: column;
align-items: stretch;
text-align: center;
flex: 1;
min-height: 0;
overflow: hidden;
}
.planning-intro {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-md);
max-width: 520px;
margin: 0 auto;
}
.planning-intro h4 {
font-size: 20px;
line-height: 1.3;
}
.planning-intro p {
max-width: 480px;
line-height: 1.6;
}
.planning-icon {
color: var(--triage);
margin-bottom: var(--space-sm);
}
.planning-description {
color: var(--text-muted);
font-size: 14px;
max-width: 480px;
line-height: 1.5;
}
.planning-textarea {
width: 100%;
min-height: 120px;
padding: var(--space-md);
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text);
font-family: inherit;
font-size: 14px;
max-height: calc(var(--space-2xl) * 20);
overflow-y: auto;
outline: none;
transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
}
.planning-textarea:focus {
border-color: var(--todo);
box-shadow: var(--focus-ring);
}
.planning-description-preview {
width: 100%;
min-height: calc(var(--space-md) * 7.5);
max-height: calc(var(--space-2xl) * 20);
padding: var(--space-md);
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text);
overflow-y: auto;
}
.planning-description-preview.expanded {
min-height: calc(var(--space-md) * 12.5);
}
.planning-comment-section {
margin-top: var(--space-lg);
}
.planning-other-answer {
margin-top: var(--space-sm);
}
.planning-other-answer .planning-textarea {
min-height: calc(var(--space-md) * 5);
}
.planning-comment-label {
display: block;
margin-bottom: var(--space-xs);
color: var(--text-muted);
font-size: 0.8rem;
}
.planning-comment-section .planning-textarea {
min-height: calc(var(--space-md) * 5);
}
.planning-char-count {
text-align: right;
font-size: 12px;
color: var(--text-muted);
margin-top: var(--space-xs);
}
.planning-initial .form-group,
.planning-summary-form .form-group {
width: 100%;
padding: 0;
margin-top: 0;
}
.planning-initial .form-group,
.planning-examples {
max-width: 520px;
margin: 0 auto;
}
.planning-initial .form-group {
text-align: left;
}
.planning-examples {
width: 100%;
}
.planning-examples-label {
font-size: 12px;
color: var(--text-muted);
margin-bottom: var(--space-sm);
display: block;
}
.planning-example-chips {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
justify-content: center;
}
.planning-example-chip {
padding: var(--space-sm) calc(var(--space-md) + var(--space-xs) * 0.5);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-pill);
font-size: calc(var(--space-sm) + var(--space-xs));
line-height: 1.4;
color: var(--text-muted);
cursor: pointer;
transition:
background var(--transition-fast),
border-color var(--transition-fast),
color var(--transition-fast),
transform var(--transition-fast),
box-shadow var(--transition-fast);
}
.planning-example-chip:hover {
background: var(--card-hover);
border-color: var(--todo);
color: var(--text);
transform: translateY(-1px);
box-shadow: var(--focus-ring);
}
.planning-start-btn {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
}
.planning-initial .planning-view-footer {
width: 100%;
max-width: 568px;
margin: 0 auto;
justify-content: center;
}
/* Question View */
.planning-question {
display: flex;
flex-direction: column;
gap: 20px;
flex: 1;
min-height: 0;
}
.planning-progress {
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.planning-progress-bar {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-sm);
}
.planning-progress-step {
height: 4px;
border-radius: var(--radius-pill);
background: var(--border);
transition: background-color var(--transition-fast);
}
.planning-progress-step.active {
background: var(--todo);
}
.planning-progress-text {
font-size: 12px;
color: var(--text-muted);
}
.planning-question-form {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.planning-question-scroll {
gap: 16px;
padding-top: 0;
}
.planning-question-panel {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 20px 20px;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
.planning-question-content {
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
.planning-question-text {
font-size: 16px;
font-weight: 600;
}
.planning-question-desc {
font-size: 14px;
color: var(--text-muted);
line-height: 1.4;
}
/* Options */
.planning-options {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.planning-option {
display: flex;
align-items: flex-start;
gap: var(--space-md);
padding: 14px 16px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
cursor: pointer;
transition:
background var(--transition-fast),
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
}
.planning-option--radio,
.planning-option--checkbox {
width: 100%;
}
.planning-option:hover {
background: var(--card-hover);
border-color: var(--todo);
box-shadow: var(--focus-ring);
transform: translateY(-1px);
}
.planning-option input[type="radio"],
.planning-option input[type="checkbox"] {
margin-top: 2px;
accent-color: var(--todo);
}
.planning-option-content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.planning-option-label {
font-weight: 500;
color: var(--text);
}
.planning-option-desc {
font-size: 12px;
color: var(--text-muted);
}
.planning-radio-group,
.planning-checkbox-group {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.planning-confirm-answer {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.planning-confirm-group {
display: flex;
gap: var(--space-md);
}
.planning-confirm-btn {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
flex: 1;
padding: var(--space-md) var(--space-xl);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--surface);
color: var(--text);
}
.planning-confirm-btn.selected {
background: var(--todo);
color: var(--bg);
border-color: var(--todo);
}
/* Actions */
.planning-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-md);
padding: var(--space-lg) var(--space-xl) var(--space-xl);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.planning-actions .btn {
flex-shrink: 0;
}
.planning-actions-primary {
margin-left: auto;
}
.planning-back-btn {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
}
/* Summary View */
.planning-summary {
display: flex;
flex-direction: column;
gap: 20px;
flex: 1;
min-height: 0;
overflow: hidden;
}
.planning-summary-header {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-md);
text-align: center;
}
.planning-summary-icon {
color: var(--color-success);
}
.planning-summary-scroll {
gap: 20px;
}
.planning-summary-qa-disclosure {
margin-top: 0;
}
.planning-summary-qa-disclosure .onboarding-disclosure-content {
padding-inline-start: 0;
}
.planning-summary-form {
display: flex;
flex-direction: column;
gap: var(--space-xl);
flex: 1;
min-height: 0;
}
.planning-summary-form .form-group {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-lg);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
.planning-summary-meta-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-md);
}
.planning-summary-meta-row .form-group {
margin: 0;
}
.planning-summary-form .form-group label {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.planning-summary-description-header {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.planning-summary-description-actions {
display: flex;
align-items: center;
gap: var(--space-md);
margin-left: auto;
}
.planning-expand-btn {
padding: 0;
background: none;
border: none;
color: var(--todo);
font-size: calc(var(--space-sm) + var(--space-xs));
font-weight: 500;
cursor: pointer;
}
.planning-expand-btn:hover {
text-decoration: underline;
}
.planning-size-label {
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
color: var(--text-muted);
}
/* Taller description textarea in subtask breakdown */
.planning-summary-form .form-group textarea {
min-height: 120px;
resize: vertical;
}
/* Compact size dropdown */
.planning-size-select {
width: 100%;
padding: calc(var(--space-sm) + var(--space-xs) * 0.5) calc(var(--space-xl) + var(--space-md)) calc(var(--space-sm) + var(--space-xs) * 0.5) var(--space-md);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text);
font-size: calc(var(--space-md) + var(--space-xs) * 0.5);
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right calc(var(--space-sm) + var(--space-xs) * 0.5) center;
}
.planning-size-select:focus {
outline: none;
border-color: var(--todo);
box-shadow: var(--focus-ring);
}
.planning-size-select:hover:not(:focus) {
border-color: var(--text-muted);
}
.planning-deps-list {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: var(--space-sm);
max-height: 180px;
overflow-y: auto;
padding-right: 4px;
overscroll-behavior: contain;
}
.planning-dep-chip {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
min-width: 0;
padding: var(--space-sm) var(--space-md);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-pill);
font-size: 12px;
cursor: pointer;
transition:
background var(--transition-fast),
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
}
.planning-dep-chip:hover {
background: var(--card-hover);
border-color: var(--todo);
box-shadow: var(--focus-ring);
transform: translateY(-1px);
}
.planning-dep-chip.selected {
background: var(--card-hover);
border-color: var(--todo);
box-shadow: inset 0 0 0 1px var(--todo);
}
.planning-dep-chip input[type="checkbox"] {
accent-color: var(--todo);
}
.planning-dep-id {
font-family: var(--font-mono);
font-weight: 600;
color: var(--text-muted);
}
.planning-dep-title {
color: var(--text);
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.planning-deliverables {
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
padding: 0;
font-size: 14px;
color: var(--text);
}
.planning-deliverables li {
position: relative;
padding-left: 16px;
line-height: 1.5;
}
.planning-deliverables li::before {
content: "•";
position: absolute;
left: 0;
color: var(--todo);
}
.planning-summary-actions {
justify-content: space-between;
min-width: 0;
}
.planning-summary-actions-right {
display: flex;
gap: var(--space-sm);
align-items: center;
min-width: 0;
max-width: 100%;
}
/*
FNXC:PlanningMode 2026-06-25-09:20:
Tablet embedded Planning keeps the desktop two-pane shell, so the summary footer must absorb the narrower detail pane instead of relying on the mobile full-width stack. Allow the three labeled actions to wrap inside the pane while preserving the desktop left/right grouping when space is available.
*/
@media (min-width: 769px) and (max-width: 1024px) {
.planning-summary-actions {
flex-wrap: wrap;
align-items: flex-start;
min-width: 0;
}
.planning-summary-actions-right {
flex: 1 1 auto;
flex-wrap: wrap;
justify-content: flex-end;
min-width: 0;
max-width: 100%;
}
.planning-summary-actions .btn {
flex-shrink: 1;
max-width: 100%;
white-space: normal;
}
}
/* Loading State */
.planning-loading {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-lg);
padding: var(--space-xl);
color: var(--text-muted);
}
/* AI Thinking Output Display */
.planning-thinking-container {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-md);
width: 100%;
max-width: 600px;
}
.planning-thinking-toggle {
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
transition:
background var(--transition-fast),
color var(--transition-fast),
border-color var(--transition-fast);
}
.planning-thinking-toggle:hover {
background: var(--card-hover);
color: var(--text);
}
.planning-thinking-output {
width: 100%;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-lg);
text-align: left;
}
.planning-thinking-output pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
color: var(--text);
}
.planning-loading-actions {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-md);
margin-top: var(--space-md);
}
.planning-stop-btn {
border-color: var(--color-error);
color: var(--color-error);
background: color-mix(in srgb, var(--color-error) 12%, transparent);
}
.planning-stop-btn:hover {
border-color: var(--color-error-dark);
color: var(--color-error-dark);
background: color-mix(in srgb, var(--color-error) 18%, transparent);
}
.planning-stop-btn {
min-height: 36px;
}
.planning-elapsed {
font-size: 12px;
color: var(--text-muted);
}
/* Subtask Drag-and-Drop Styles */
.subtask-item {
transition: opacity var(--transition-fast), transform var(--transition-fast);
}
.subtask-item-dragging {
opacity: 0.5;
transform: scale(0.98);
}
.subtask-item-drop-target {
background: color-mix(in srgb, var(--todo) 12%, transparent);
border-color: var(--todo);
}
.subtask-item-drop-before {
position: relative;
}
.subtask-item-drop-before::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--todo);
z-index: 1;
}
.subtask-item-drop-after {
position: relative;
}
.subtask-item-drop-after::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--todo);
z-index: 1;
}
.subtask-item-header {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.subtask-drag-handle {
display: flex;
align-items: center;
gap: var(--space-sm);
cursor: grab;
color: var(--text-dim);
transition: color var(--transition-fast);
user-select: none;
}
.subtask-drag-handle:hover {
color: var(--text-muted);
}
.subtask-item-dragging .subtask-drag-handle {
cursor: grabbing;
color: var(--todo);
}
.subtask-item-actions {
display: flex;
align-items: center;
gap: var(--space-xs);
}
.subtask-item-actions .btn-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-md);
color: var(--text-dim);
cursor: pointer;
transition: all var(--transition-fast);
}
@media (hover: hover) {
.subtask-item-actions .btn-icon:hover:not(:disabled) {
background: var(--card-hover);
border-color: var(--border);
color: var(--text);
}
}
.subtask-item-actions .btn-icon:disabled {
opacity: 0.3;
cursor: not-allowed;
}
/* Responsive */
@media (max-width: 768px) {
/* FNXC:PlanningMode 2026-06-22-15:30: this legacy full-viewport sheet sizing
is for the dialog presentation only. The :not(.planning-modal--embedded)
guard prevents the embedded view from being yanked to 100vw/100dvh, which
would overflow the main-content pane it lives in on mobile. */
.planning-modal:not(.planning-modal--embedded) {
width: 100vw;
height: 100vh;
height: 100dvh;
min-height: 100vh;
min-height: 100dvh;
max-width: 100%;
max-height: 100%;
border-radius: 0;
}
.planning-error {
margin: var(--space-lg) var(--space-lg) 0;
}
.planning-view-scroll {
padding: var(--space-lg);
gap: var(--space-lg);
}
.planning-question-scroll {
padding-top: var(--space-sm);
gap: var(--space-md);
}
.planning-view-footer,
.planning-actions {
padding: var(--space-md) var(--space-lg) calc(var(--space-lg) + env(safe-area-inset-bottom));
}
.planning-actions {
flex-direction: column;
align-items: stretch;
}
.planning-actions .btn,
.planning-start-btn {
width: 100%;
justify-content: center;
}
.planning-actions-primary {
margin-left: 0;
}
.planning-summary-actions-right {
flex-direction: column;
align-items: stretch;
width: 100%;
}
.planning-summary-meta-row {
grid-template-columns: minmax(0, 1fr);
}
.planning-summary-description-header {
align-items: flex-start;
flex-wrap: wrap;
}
.planning-summary-description-actions {
margin-left: 0;
margin-inline-start: auto;
}
.planning-example-chips {
flex-direction: column;
align-items: stretch;
}
.planning-example-chip {
width: 100%;
text-align: left;
border-radius: var(--radius-lg);
}
.planning-depth-selector {
gap: var(--space-sm);
}
.planning-depth-controls-row {
flex-direction: column;
align-items: stretch;
}
.planning-depth-chip-group {
justify-content: flex-start;
}
.planning-depth-question-count {
align-items: flex-start;
}
.planning-deps-list {
flex-direction: column;
flex-wrap: nowrap;
max-height: 200px;
}
.planning-dep-chip {
width: 100%;
padding: 10px 12px;
border-radius: var(--radius-lg);
}
.planning-dep-title {
max-width: none;
}
/* Planning thinking output mobile adjustments */
.planning-thinking-container {
max-width: 100%;
}
.planning-thinking-output {
max-height: 200px;
padding: var(--space-md);
}
.planning-thinking-output pre {
font-size: 12px;
}
.planning-loading-actions {
flex-direction: column;
width: 100%;
}
.planning-loading-actions .btn {
width: 100%;
min-height: 36px;
justify-content: center;
}
/* Prevent mobile zoom on focus for planning text inputs (16px minimum) */
.planning-textarea {
font-size: 16px;
}
.planning-comment-section .planning-textarea,
.planning-other-answer .planning-textarea {
min-height: calc(var(--space-md) * 5);
}
/* Planning options compact on mobile */
.planning-option {
padding: 12px;
}
/* Subtask breakdown: touch-friendly drag handles with padding */
.subtask-drag-handle {
min-width: 36px;
min-height: 36px;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
}
/* Subtask action buttons: mobile-friendly sizing */
.subtask-item-actions .btn-icon {
min-width: 36px;
min-height: 36px;
}
/* Subtask items: prevent overflow */
.subtask-item-header {
flex-wrap: wrap;
gap: var(--space-xs);
}
/* Dependency chips: touch-friendly */
.planning-dep-chip {
min-height: 36px;
}
/* Size select: prevent iOS zoom */
.planning-size-select {
font-size: 16px;
}
/* Progress bar compact on narrow screens */
.planning-progress-bar {
gap: var(--space-xs);
}
.planning-progress-text {
font-size: 11px;
}
/* Confirm buttons: mobile-friendly sizing */
.planning-confirm-group {
flex-direction: column;
}
.planning-confirm-btn {
min-height: 36px;
padding: var(--space-sm) var(--space-md);
}
/* Model selection: rows don't overflow */
.planning-modal-body .inline-create-model-row {
gap: var(--space-sm);
}
.planning-modal-body .model-badge {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
/* Model selection: prevent iOS zoom on select elements */
.planning-modal-body select {
font-size: 16px;
}
}
/* === Planning & Breakdown Modal — Inline Style Replacements === */
/* Icon color utilities */
.icon-triage {
color: var(--triage);
}
.icon-triage-lg {
color: var(--triage);
margin-bottom: var(--space-md);
}
.icon-todo {
color: var(--todo);
}
.icon-success {
color: var(--color-success);
}
.icon-text-secondary {
color: var(--text-muted);
flex-shrink: 0;
}
.icon-milestone {
color: var(--icon-milestone);
flex-shrink: 0;
}
.icon-slice {
color: var(--icon-slice);
flex-shrink: 0;
}
.icon-feature {
color: var(--icon-feature);
margin-top: var(--space-xs);
flex-shrink: 0;
}
/* Icon margin utilities */
.icon-mr-4 {
margin-right: var(--space-xs);
}
.icon-mr-6 {
margin-right: calc(var(--space-sm) - var(--space-xs) * 0.5);
}
.icon-mr-8 {
margin-right: var(--space-sm);
}
.icon-ml-4 {
margin-left: var(--space-xs);
}
.icon-ml-6 {
margin-left: calc(var(--space-sm) - var(--space-xs) * 0.5);
}
/* Error panel */
.ai-error-panel {
border: 1px solid var(--color-error);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--color-error) 10%, transparent);
padding: calc(var(--space-md) + var(--space-xs) * 0.5);
display: grid;
gap: calc(var(--space-md) - var(--space-xs) * 0.5);
}
.ai-error-icon {
font-size: calc(var(--space-lg) + var(--space-xs));
}
.ai-error-actions {
display: flex;
gap: var(--space-sm);
}
/* Text link button */
.text-link-btn {
font-size: calc(var(--space-sm) + var(--space-xs));
background: none;
border: none;
color: inherit;
text-decoration: underline;
cursor: pointer;
padding: 0;
}
/* Form hint error variant */
.form-hint-error {
margin-top: calc(var(--space-sm) - var(--space-xs) * 0.5);
font-size: calc(var(--space-sm) + var(--space-xs));
color: var(--color-error);
}
/* Text muted small */
.text-muted-sm {
margin-left: var(--space-sm);
font-size: calc(var(--space-sm) + var(--space-xs));
}
/* Model selector spacing */
.model-select-row--left {
margin-right: auto;
min-width: min(100%, 320px);
max-width: 420px;
text-align: left;
}
.model-selector-current--spaced {
margin-top: var(--space-sm);
}
/* Subtask header override */
.subtask-item-header--between {
justify-content: space-between;
}