Fixes ~19 deterministic dashboard test failures pre-existing on origin/main (non-blocking full-suite lane). Mix of real product fixes and stale-test reconciliation; no appeasement (no widened timeouts, skips, or weakened assertions). Product CSS fixes (real regressions the guards caught): - Info toast contrast: shadcn-custom light theme had white-on-#0284c7 (4.10, below WCAG AA 4.5); enrolled it in the light-mode dark-text correction. - 27 undefined CSS token references (typos/foreign tokens) renamed to canonical defined tokens; defined the genuinely-intended --border-strong and --right-dock-min/max-width. - Raw rgba() box-shadow fallback tokenized to color-mix; dev-server mobile header split into its own responsive rule. Stale tests reconciled with intentional product changes: - workflowColumns graduated to always-on (board-workflows unit test). - workflow optional-steps source re-pointed to v2 optional-group nodes. - command-center pricing docs (one doc gap filled: openai-codex:* keying). - SetupWizardModal added detectWorkspace + workspaceMode/taskPrefix payload. - board-mobile listener-count assertion → unmount no-throw behavior. - CommandCenterControls / ThemeSelector: default theme relabeled "Fusion Legacy". - ProjectOverview / WorkflowNodeEditor: header divider intentionally removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1344 lines
36 KiB
CSS
1344 lines
36 KiB
CSS
/*
|
||
FNXC:Terminal 2026-06-13-20:02:
|
||
The symbols-only Nerd Font must stay unicode-range-scoped to Nerd Font codepoints only; otherwise mobile DOM/canvas xterm can measure ASCII cells against the symbols font after font-display: swap and render commands like `pnpm build` with oversized inter-character spacing.
|
||
|
||
FNXC:Terminal 2026-06-17-18:12:
|
||
FN-6603 found that unicode-range scoping is not enough when the symbols face is first: iOS canvas measurement can still use that first face for xterm cell metrics while DOM glyph fallback draws ASCII from a later monospace font. Keep text fonts first in terminalPreferences and this symbols face as a fallback so powerline glyphs remain available without corrupting ASCII cell width.
|
||
|
||
FNXC:Terminal 2026-06-20-18:04:
|
||
FN-6811 recurrence #6 tightened ownership of this scoped symbols face: every terminal surface that exposes the DOM glyph fallback must define the same ASCII-excluding @font-face in its own CSS chunk, and tests must check each CSS owner independently rather than relying on combined app CSS.
|
||
*/
|
||
@font-face {
|
||
font-family: "Fusion Terminal Nerd Font Symbols";
|
||
src: url("/fonts/SymbolsNerdFontMono-Regular.ttf") format("truetype");
|
||
font-display: swap;
|
||
unicode-range: U+23FB-23FE, U+2665, U+26A1, U+2B58, U+E000-E00A, U+E0A0-E0D7, U+E200-E2A9, U+E300-E3E3, U+E5FA-E6B7, U+E700-E8EF, U+EA60-EC1E, U+ED00-F2FF, U+F300-F533, U+F0001-F1AF0;
|
||
}
|
||
|
||
/* === Terminal Modal === */
|
||
/* Center vertically (override the overlay's default top alignment). */
|
||
.modal-overlay:has(.terminal-modal) {
|
||
align-items: center;
|
||
padding-top: 0;
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
/*
|
||
FNXC:Terminal 2026-06-21-22:32:
|
||
FN-6887 turns desktop/tablet terminal into a bottom-docked panel above the footer. Keep mobile on the fullscreen modal path through the max-width media override below.
|
||
|
||
FNXC:Terminal 2026-06-22-00:00:
|
||
The docked/floating terminal must not blur or dim the page behind it, and the page must stay interactive. The base .modal-overlay applies backdrop-filter: blur(4px); override it to none here. background is already transparent and pointer-events:none lets clicks pass through to the page behind (the terminal panel itself re-enables pointer-events).
|
||
*/
|
||
/*
|
||
FNXC:Terminal 2026-06-22-00:45:
|
||
The override MUST out-specify the base `.modal-overlay` (which sets a dimmed background + blur). Both are single-class selectors, so if styles.css loads after this file the dim/blur wins and the page still fades. Qualify with `.modal-overlay` (two classes) so the docked/floating terminal reliably keeps a transparent, non-blurring, click-through backdrop regardless of stylesheet order.
|
||
*/
|
||
/*
|
||
FNXC:Terminal 2026-06-23-04:30:
|
||
The terminal must NEVER dim or blur the page behind it, in ANY mode. The base .modal-overlay applies a dimmed background + blur; the docked/floating overrides killed it, but a terminal that is NEITHER (the mobile/default sheet) still fell back to the dim. This base terminal-overlay rule removes the dim+blur for every terminal state.
|
||
|
||
FNXC:Terminal 2026-06-23-21:28:
|
||
Theme-level modal backdrop rules can load after component CSS and reapply dim/blur, especially in glass themes. Use a higher-specificity terminal overlay selector and clear both standard and WebKit backdrop filters so docked and modal terminal surfaces never darken the app behind them.
|
||
*/
|
||
.modal-overlay.terminal-modal-overlay.terminal-modal-overlay {
|
||
background: transparent;
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
.modal-overlay.terminal-modal-overlay--docked.terminal-modal-overlay--docked,
|
||
.modal-overlay.terminal-modal-overlay--floating.terminal-modal-overlay--floating {
|
||
align-items: stretch;
|
||
justify-content: flex-end;
|
||
padding: 0;
|
||
background: transparent;
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/*
|
||
FNXC:FloatingWindow 2026-06-22-21:30:
|
||
Only the FLOATING terminal joins the shared cross-type floating stack. Reset the base `.modal-overlay` z-index:100 to auto so this click-through overlay does NOT establish a stacking context; the floating panel's inline z-index (from floatingWindowStack, 4000+) then interleaves at the root with the right-dock pop-out, the floating New Task dialog, and FloatingWindow. Docked mode keeps the base overlay stacking (full-width bottom panel) and is intentionally excluded.
|
||
*/
|
||
.modal-overlay.terminal-modal-overlay--floating {
|
||
z-index: auto;
|
||
}
|
||
|
||
.modal.terminal-modal {
|
||
/* Initial dimensions are applied only when no persisted size has been
|
||
restored — see :not([style*=...]) selectors below. */
|
||
min-width: 480px;
|
||
min-height: 320px;
|
||
max-width: calc(100vw - (var(--space-xl) * 2));
|
||
max-height: calc(100dvh - 40px);
|
||
background: var(--card);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
resize: both;
|
||
}
|
||
|
||
.modal.terminal-modal:not([style*="width"]) {
|
||
width: min(1800px, calc(100vw - (var(--space-xl) * 2)));
|
||
}
|
||
.modal.terminal-modal:not([style*="height"]) {
|
||
height: min(85vh, calc(100dvh - 40px));
|
||
}
|
||
|
||
.modal.terminal-modal.terminal-modal--docked {
|
||
--floating-window-shadow: var(--shadow-lg);
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: calc(var(--icb-bottom-offset, 0px) + var(--executor-footer-height, calc(var(--space-xl) + var(--space-md))));
|
||
width: 100vw;
|
||
min-width: 0;
|
||
height: var(--terminal-docked-height);
|
||
min-height: calc(var(--space-xl) * 10);
|
||
max-width: none;
|
||
max-height: calc(100dvh - (var(--space-xl) * 4));
|
||
resize: none;
|
||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||
pointer-events: auto;
|
||
box-shadow: var(--floating-window-shadow, var(--shadow-lg));
|
||
}
|
||
|
||
/*
|
||
FNXC:Terminal 2026-06-22-01:30:
|
||
Larger grab target for the docked terminal top resize handle: it straddles the panel's top edge (extends above it) and is taller, so it is easy to grab on desktop and touch. touch-action:none keeps the drag from being hijacked by scroll/gestures so the resize stays smooth.
|
||
*/
|
||
.terminal-docked-resize-handle {
|
||
position: absolute;
|
||
top: calc(var(--space-sm) * -1);
|
||
left: 0;
|
||
right: 0;
|
||
height: calc(var(--space-md) + var(--space-sm));
|
||
cursor: ns-resize;
|
||
touch-action: none;
|
||
z-index: 2;
|
||
}
|
||
|
||
.terminal-docked-resize-handle::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 50%;
|
||
top: calc(var(--space-xs) / 2);
|
||
width: calc(var(--space-xl) * 2);
|
||
height: calc(var(--space-xs) / 2);
|
||
transform: translateX(-50%);
|
||
border-radius: var(--radius-pill);
|
||
background: var(--border);
|
||
}
|
||
|
||
.modal.terminal-modal.terminal-modal--floating {
|
||
--floating-window-shadow: var(--shadow-lg);
|
||
position: fixed;
|
||
left: var(--terminal-float-x);
|
||
top: var(--terminal-float-y);
|
||
width: var(--terminal-float-width);
|
||
height: var(--terminal-float-height);
|
||
min-width: calc(var(--space-xl) * 20);
|
||
min-height: calc(var(--space-xl) * 13.333);
|
||
max-width: calc(100vw - (var(--space-lg) * 2));
|
||
max-height: calc(100dvh - (var(--space-lg) * 2));
|
||
resize: none;
|
||
pointer-events: auto;
|
||
/*
|
||
FNXC:FloatingWindow 2026-06-23-23:32:
|
||
Floating modals share a gentle theme-controlled elevation token. Use the app shadow fallback instead of undefined --shadow-xl so themes can opt out or tune shadow strength consistently across Terminal, New Task, Right Dock, and shared FloatingWindow panels.
|
||
*/
|
||
box-shadow: var(--floating-window-shadow, var(--shadow-lg));
|
||
}
|
||
|
||
/*
|
||
FNXC:Terminal 2026-06-22-19:50:
|
||
The floating-mode header is the move grip. `touch-action: none` is required so a touch-drag on it is delivered as a continuous pointermove stream (paired with setPointerCapture on the captured element) instead of being hijacked by the browser into page scroll/pan. Without it the floating drag stutters on touch — same fix the right-dock pop-out drag handle uses. cursor: grab/grabbing signals the move affordance on desktop.
|
||
*/
|
||
.terminal-header--draggable {
|
||
cursor: grab;
|
||
user-select: none;
|
||
touch-action: none;
|
||
min-height: 48px;
|
||
}
|
||
|
||
.terminal-header--draggable:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.terminal-floating-resize-handle {
|
||
position: absolute;
|
||
z-index: 2;
|
||
touch-action: none;
|
||
}
|
||
|
||
.terminal-floating-resize-handle--n,
|
||
.terminal-floating-resize-handle--s {
|
||
left: var(--space-sm);
|
||
right: var(--space-sm);
|
||
height: var(--space-sm);
|
||
cursor: ns-resize;
|
||
}
|
||
|
||
.terminal-floating-resize-handle--n { top: 0; }
|
||
.terminal-floating-resize-handle--s { bottom: 0; }
|
||
|
||
.terminal-floating-resize-handle--e,
|
||
.terminal-floating-resize-handle--w {
|
||
top: var(--space-sm);
|
||
bottom: var(--space-sm);
|
||
width: var(--space-sm);
|
||
cursor: ew-resize;
|
||
}
|
||
|
||
.terminal-floating-resize-handle--e { right: 0; }
|
||
.terminal-floating-resize-handle--w { left: 0; }
|
||
|
||
.terminal-floating-resize-handle--ne,
|
||
.terminal-floating-resize-handle--nw,
|
||
.terminal-floating-resize-handle--se,
|
||
.terminal-floating-resize-handle--sw {
|
||
width: var(--space-lg);
|
||
height: var(--space-lg);
|
||
}
|
||
|
||
.terminal-floating-resize-handle--ne { top: 0; right: 0; cursor: nesw-resize; }
|
||
.terminal-floating-resize-handle--nw { top: 0; left: 0; cursor: nwse-resize; }
|
||
.terminal-floating-resize-handle--se { bottom: 0; right: 0; cursor: nwse-resize; }
|
||
.terminal-floating-resize-handle--sw { bottom: 0; left: 0; cursor: nesw-resize; }
|
||
|
||
.terminal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--surface);
|
||
padding: 0;
|
||
gap: 0;
|
||
}
|
||
|
||
.terminal-tabs {
|
||
display: flex;
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow-x: auto;
|
||
gap: 0;
|
||
padding: 0 4px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--border) transparent;
|
||
}
|
||
|
||
.terminal-tabs::-webkit-scrollbar {
|
||
height: 4px;
|
||
}
|
||
|
||
.terminal-tabs::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.terminal-tabs::-webkit-scrollbar-thumb {
|
||
background: var(--border);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.terminal-tab {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-sm);
|
||
padding: var(--space-md) var(--space-lg);
|
||
background: none;
|
||
border: none;
|
||
border-bottom: 3px solid transparent;
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: color var(--transition-fast), border-color var(--transition-fast);
|
||
position: relative;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.terminal-tab:hover {
|
||
color: var(--text);
|
||
background: var(--card-hover);
|
||
}
|
||
|
||
.terminal-tab--active {
|
||
color: var(--text);
|
||
border-bottom-color: var(--in-progress);
|
||
}
|
||
|
||
.terminal-tab--active:hover {
|
||
border-bottom-color: var(--in-progress);
|
||
}
|
||
|
||
.terminal-tab-indicator {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 3px;
|
||
background: var(--in-progress);
|
||
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||
}
|
||
|
||
.terminal-tab-label {
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.terminal-tab-close {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 18px;
|
||
height: 18px;
|
||
padding: 0;
|
||
margin-left: 4px;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-muted);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
transition: opacity var(--transition-fast), background var(--transition-fast), color var(--transition-fast);
|
||
}
|
||
|
||
.terminal-tab:hover .terminal-tab-close,
|
||
.terminal-tab--active .terminal-tab-close {
|
||
opacity: 1;
|
||
}
|
||
|
||
.terminal-tab-close:hover {
|
||
background: var(--card-hover);
|
||
color: var(--text);
|
||
}
|
||
|
||
.terminal-tab--new {
|
||
color: var(--text-muted);
|
||
background: none;
|
||
border: 1px dashed var(--border);
|
||
border-radius: var(--radius-sm);
|
||
min-width: 32px;
|
||
justify-content: center;
|
||
padding: var(--space-xs) var(--space-sm);
|
||
}
|
||
|
||
.terminal-tab--new:hover {
|
||
color: var(--text);
|
||
background: var(--card-hover);
|
||
border-color: var(--text-muted);
|
||
}
|
||
|
||
.terminal-tab--empty {
|
||
color: var(--text-muted);
|
||
cursor: default;
|
||
font-style: italic;
|
||
}
|
||
|
||
.terminal-tab--empty:hover {
|
||
background: none;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.terminal-close {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 44px;
|
||
height: 44px;
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
transition: color var(--transition-fast), background var(--transition-fast);
|
||
}
|
||
|
||
.terminal-close:hover {
|
||
color: var(--text);
|
||
background: var(--card-hover);
|
||
}
|
||
|
||
/*
|
||
FNXC:Terminal 2026-06-23-22:12:
|
||
The terminal header pop-out/dock affordance is an icon-only utility control. It should not carry button background or border chrome; keep hover/focus feedback only through color/focus ring so it reads like the surrounding header utility icons.
|
||
*/
|
||
.terminal-clear-btn.terminal-clear-btn--icon[data-testid="terminal-popout-toggle"] {
|
||
background: transparent;
|
||
border: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.terminal-clear-btn.terminal-clear-btn--icon[data-testid="terminal-popout-toggle"]:hover {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text);
|
||
}
|
||
|
||
.terminal-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background: var(--bg);
|
||
}
|
||
|
||
.terminal-empty-state {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--space-md);
|
||
color: var(--text-muted);
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.terminal-empty-state p {
|
||
margin: 0;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.terminal-empty-state p:first-child {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: var(--text);
|
||
}
|
||
|
||
.terminal-empty-state p:last-child {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.terminal-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: var(--space-md) var(--space-lg);
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--surface);
|
||
}
|
||
|
||
.terminal-task-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-md);
|
||
min-width: 0;
|
||
}
|
||
|
||
.terminal-task-id {
|
||
font-family: var(--font-mono);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.terminal-task-title {
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
min-width: 0;
|
||
}
|
||
|
||
.terminal-clear-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all var(--transition-fast);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.terminal-clear-btn:hover {
|
||
color: var(--text);
|
||
border-color: var(--text-muted);
|
||
background: var(--card-hover);
|
||
}
|
||
|
||
.terminal-clear-btn--shortcut {
|
||
margin-left: var(--space-xs);
|
||
}
|
||
|
||
/* FNXC:Terminal 2026-06-23-00:15: Icon-only pop-out/dock toggle — no visible text label, so render as a square icon button while keeping the same hover/border treatment as the other header controls. */
|
||
.terminal-clear-btn--icon {
|
||
padding: 6px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.terminal-log-container {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.terminal-log-container .agent-log-viewer {
|
||
flex: 1;
|
||
max-height: none;
|
||
background: var(--card);
|
||
border-radius: 0;
|
||
}
|
||
|
||
/* === Interactive Terminal Styles === */
|
||
|
||
.terminal-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-sm);
|
||
padding: 0 16px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--text);
|
||
flex: 1;
|
||
}
|
||
|
||
.terminal-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
}
|
||
|
||
.terminal-output {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: var(--space-lg);
|
||
background: var(--bg);
|
||
font-family: var(--font-mono);
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.terminal-welcome {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--space-lg);
|
||
padding: 40px 20px;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
height: 100%;
|
||
}
|
||
|
||
.terminal-welcome-icon {
|
||
color: var(--in-progress);
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.terminal-welcome h3 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.terminal-welcome p {
|
||
margin: 0;
|
||
max-width: 400px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.terminal-commands-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--space-sm);
|
||
justify-content: center;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.terminal-commands-list span {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
font-size: 12px;
|
||
font-family: var(--font-mono);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.terminal-shortcuts {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.terminal-shortcuts kbd {
|
||
display: inline-block;
|
||
padding: 2px 6px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
color: var(--text);
|
||
}
|
||
|
||
.terminal-history {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-lg);
|
||
}
|
||
|
||
.terminal-entry {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-sm);
|
||
}
|
||
|
||
.terminal-prompt-line {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-sm);
|
||
}
|
||
|
||
.terminal-prompt {
|
||
color: var(--color-success);
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.terminal-command {
|
||
color: var(--text);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.terminal-output-text {
|
||
margin: 0;
|
||
padding: var(--space-md) var(--space-lg);
|
||
background: var(--card);
|
||
border-radius: var(--radius);
|
||
color: var(--text);
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.terminal-output-error {
|
||
background: color-mix(in srgb, var(--color-error) 10%, transparent);
|
||
border-left: 3px solid var(--color-error);
|
||
}
|
||
|
||
.terminal-running-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-sm);
|
||
padding: var(--space-sm) var(--space-lg);
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.terminal-spinner {
|
||
width: 12px;
|
||
height: 12px;
|
||
border: 2px solid var(--border);
|
||
border-top-color: var(--in-progress);
|
||
border-radius: 50%;
|
||
animation: terminal-spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes terminal-spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.terminal-input-area {
|
||
border-top: 1px solid var(--border);
|
||
background: var(--surface);
|
||
padding: var(--space-md) var(--space-lg);
|
||
}
|
||
|
||
.terminal-form {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-sm);
|
||
}
|
||
|
||
.terminal-input-prompt {
|
||
color: var(--color-success);
|
||
font-weight: 600;
|
||
font-family: var(--font-mono);
|
||
font-size: 13px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.terminal-input {
|
||
flex: 1;
|
||
padding: var(--space-sm) var(--space-md);
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
color: var(--text);
|
||
font-family: var(--font-mono);
|
||
font-size: 13px;
|
||
outline: none;
|
||
transition: border-color var(--transition-fast);
|
||
}
|
||
|
||
.terminal-input:focus {
|
||
border-color: var(--in-progress);
|
||
}
|
||
|
||
.terminal-input:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.terminal-input::placeholder {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.terminal-kill-btn {
|
||
padding: var(--space-sm) var(--space-lg);
|
||
background: var(--color-error);
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
color: white;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: opacity var(--transition-fast);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.terminal-kill-btn:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
/* === PTY Terminal Styles === */
|
||
|
||
.terminal-container {
|
||
flex: 1;
|
||
min-height: 0;
|
||
position: relative;
|
||
background: var(--terminal-bg);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.terminal-xterm {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 0;
|
||
padding: var(--space-xs);
|
||
}
|
||
|
||
/*
|
||
FNXC:Terminal 2026-06-18-07:04:
|
||
Real iOS Safari recurrence #4 kept wide ASCII cells even after unicode-range scoping and symbols-last ordering; the supplied harness measured AGENTS.md at the same 66.76px for every stack, so ordering is ruled out. Pin text-size adjustment across xterm's measurement subtree so mobile WebKit cannot inflate DOM/canvas cell metrics, while WebGL/desktop keep the same 100% text scale and every terminal preset still resolves through terminalPreferences.
|
||
*/
|
||
.terminal-xterm,
|
||
.terminal-xterm * {
|
||
-webkit-text-size-adjust: 100%;
|
||
text-size-adjust: 100%;
|
||
}
|
||
|
||
/*
|
||
FNXC:Terminal 2026-06-18-15:44:
|
||
FN-6659 keeps the loaded symbols @font-face out of xterm's measured font option because every FN-6638 diagnostic stack that measured 66.76px still included that face. Limit the symbols fallback to DOM renderer row spans via a CSS custom property so ASCII measurement, fit, refresh, and WebGL/canvas option paths stay symbols-free while powerline glyphs can still resolve when xterm emits DOM text nodes.
|
||
*/
|
||
.terminal-xterm .xterm-rows span {
|
||
font-family: var(--terminal-glyph-font-family, inherit) !important;
|
||
}
|
||
|
||
/*
|
||
* xterm fit may apply inline pixel heights on the `.xterm` root after
|
||
* row/line-height recomputation (e.g. after font-size changes). Keep the root
|
||
* stretched to the wrapper height so the terminal always fills the modal.
|
||
*/
|
||
.terminal-xterm .xterm {
|
||
height: 100% !important;
|
||
min-height: 100%;
|
||
}
|
||
|
||
/*
|
||
* Keep xterm's helper textarea tiny and inside terminal bounds.
|
||
*
|
||
* Mobile browsers are more likely to honor focus for keyboard activation when
|
||
* the focused element is not placed far off-screen.
|
||
*/
|
||
.terminal-xterm .xterm .xterm-helper-textarea {
|
||
left: 0 !important;
|
||
top: 0 !important;
|
||
width: 1px !important;
|
||
height: 1px !important;
|
||
opacity: 0.01 !important;
|
||
pointer-events: auto !important;
|
||
z-index: 1 !important;
|
||
}
|
||
|
||
/*
|
||
* On touch-primary devices (mobile/tablet), expand the helper textarea to
|
||
* cover the entire terminal surface so taps land directly on it. iOS Safari
|
||
* reliably opens the soft keyboard and routes input events only when focus
|
||
* originates from a tap on the focused element itself — programmatic focus()
|
||
* from a parent's touch handler is often silently ignored. The textarea
|
||
* remains visually invisible via opacity:0 and sits above the canvas so
|
||
* tap/drag events land on it. Desktop (mouse) keeps the 1×1 rule so xterm
|
||
* can still handle drag-to-select and mouse-tracking on the canvas.
|
||
*/
|
||
@media (hover: none) and (pointer: coarse) {
|
||
.terminal-xterm .xterm .xterm-helper-textarea {
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
opacity: 0 !important;
|
||
z-index: 2 !important;
|
||
}
|
||
}
|
||
|
||
.terminal-loading {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 2;
|
||
background: var(--terminal-bg);
|
||
gap: var(--space-lg);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.terminal-loading .terminal-spinner {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 3px solid var(--border);
|
||
border-top-color: var(--in-progress);
|
||
border-radius: 50%;
|
||
animation: terminal-spin 1s linear infinite;
|
||
}
|
||
|
||
.terminal-shell-name {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
font-family: "SF Mono", Monaco, Consolas, monospace;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.terminal-status {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.terminal-status.connected {
|
||
background: var(--done);
|
||
}
|
||
|
||
.terminal-status.connecting,
|
||
.terminal-status.reconnecting {
|
||
background: var(--in-progress);
|
||
animation: terminal-pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
.terminal-status.disconnected {
|
||
background: var(--color-error);
|
||
}
|
||
|
||
@keyframes terminal-pulse {
|
||
0%, 100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
|
||
/*
|
||
FNXC:Terminal 2026-06-22-17:15:
|
||
The shortcut bar (modifier keys + arrow keys) must sit on ONE line, not stack into separate rows. The panel no longer wraps; the modifier-row and arrow-row are inline (intrinsic width, no 100% / margin-bottom), and the panel scrolls horizontally if the buttons exceed the width.
|
||
*/
|
||
.terminal-shortcut-panel {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
align-items: center;
|
||
gap: var(--space-xs);
|
||
padding: var(--space-xs) var(--space-sm);
|
||
background: var(--surface);
|
||
border-top: 1px solid var(--border);
|
||
overflow-x: auto;
|
||
/*
|
||
FNXC:Terminal 2026-06-22-22:00:
|
||
On a narrow folded phone the modifier/arrow/letter keys exceed the viewport width, so the bar MUST scroll horizontally to keep every button reachable. touch-action: pan-x lets a horizontal swipe scroll the row (instead of the browser hijacking it as a page gesture), overscroll-behavior-x: contain stops the swipe from bleeding into page/back-navigation at the ends, and -webkit-overflow-scrolling: touch gives momentum scroll on iOS.
|
||
*/
|
||
touch-action: pan-x;
|
||
overscroll-behavior-x: contain;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.terminal-shortcut-modifier-row,
|
||
.terminal-shortcut-arrow-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-xs);
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.terminal-shortcut-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
/*
|
||
FNXC:Terminal 2026-06-22-22:00:
|
||
Keys keep their intrinsic width and never shrink/grow, so the row's total width exceeds a narrow viewport and the panel's overflow-x: auto produces a real horizontal scroll reaching the rightmost buttons. flex:1 / width:100% here would collapse every key to fit the viewport and defeat the scroll.
|
||
*/
|
||
flex: 0 0 auto;
|
||
min-width: calc(var(--space-xl) + var(--space-xs));
|
||
min-height: calc(var(--space-xl) + var(--space-xs));
|
||
padding: 0 var(--space-xs);
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text);
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
|
||
}
|
||
|
||
.terminal-shortcut-btn:hover {
|
||
background: var(--card-hover);
|
||
}
|
||
|
||
.terminal-shortcut-btn:focus-visible {
|
||
outline: none;
|
||
box-shadow: var(--focus-ring-strong);
|
||
}
|
||
|
||
.terminal-shortcut-btn--modifier {
|
||
min-width: calc(var(--space-xl) + var(--space-md) + var(--space-xs));
|
||
}
|
||
|
||
.terminal-shortcut-btn--modifier.is-active {
|
||
background: var(--in-progress);
|
||
color: var(--card);
|
||
border-color: var(--in-progress);
|
||
}
|
||
|
||
.terminal-preferences-panel {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(calc(var(--space-2xl) * 4), 1fr));
|
||
gap: var(--space-sm);
|
||
padding: var(--space-sm);
|
||
background: var(--surface);
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.terminal-preference-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-xs);
|
||
color: var(--text-muted);
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.terminal-preference-field--checkbox {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
align-self: end;
|
||
color: var(--text);
|
||
}
|
||
|
||
.terminal-preference-control {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.terminal-preference-note {
|
||
color: var(--text-muted);
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.terminal-preferences-reset {
|
||
align-self: end;
|
||
justify-self: start;
|
||
}
|
||
|
||
/*
|
||
FNXC:Terminal 2026-06-23-00:15:
|
||
Footer reads left-to-right: text-size control, then the relocated Clear/Shortcuts/Preferences action cluster, then connection status / exit code / zoom-hint pushed to the right edge (margin-left:auto on the connection status). flex-wrap keeps the action cluster from clipping when the docked/floating panel is narrow; gap replaces the old space-between distribution.
|
||
*/
|
||
.terminal-status-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: var(--space-sm);
|
||
padding: var(--space-sm) var(--space-lg);
|
||
background: var(--surface);
|
||
border-top: 1px solid var(--border);
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* FNXC:Terminal 2026-06-23-00:15: Grouped cluster of the relocated Clear/Shortcuts/Preferences buttons; scrolls horizontally as a unit on very narrow footers so the buttons stay reachable instead of clipping. */
|
||
.terminal-footer-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-xs);
|
||
min-width: 0;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
/* FNXC:Terminal 2026-06-23-00:15: Connection status starts the right-aligned trailing group (status + exit code + zoom hint) so the left edge holds the text-size and action controls. */
|
||
.terminal-connection-status {
|
||
font-weight: 500;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.terminal-connection-status.connected {
|
||
color: var(--done);
|
||
}
|
||
|
||
.terminal-connection-status.connecting,
|
||
.terminal-connection-status.reconnecting {
|
||
color: var(--in-progress);
|
||
}
|
||
|
||
.terminal-connection-status.disconnected {
|
||
color: var(--color-error);
|
||
}
|
||
|
||
.terminal-exit-code {
|
||
color: var(--color-error);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.terminal-font-size-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--space-xs);
|
||
}
|
||
|
||
.terminal-font-size-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: calc(var(--space-lg) + var(--space-md) + var(--space-xs));
|
||
min-height: calc(var(--space-lg) + var(--space-md) + var(--space-xs));
|
||
padding: 0;
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-md);
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
|
||
}
|
||
|
||
.terminal-font-size-btn:hover {
|
||
background: var(--card-hover);
|
||
border-color: var(--text-muted);
|
||
}
|
||
|
||
.terminal-font-size-btn:focus-visible {
|
||
outline: none;
|
||
box-shadow: var(--focus-ring-strong);
|
||
}
|
||
|
||
.terminal-font-size-value {
|
||
min-width: calc(var(--space-xl) + var(--space-xs));
|
||
text-align: center;
|
||
font-family: var(--font-mono);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.terminal-reconnect-btn,
|
||
.terminal-restart-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-xs);
|
||
padding: 6px 12px;
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
color: var(--text);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
.terminal-reconnect-btn:hover,
|
||
.terminal-restart-btn:hover {
|
||
background: var(--card-hover);
|
||
border-color: var(--text-muted);
|
||
}
|
||
|
||
.terminal-error {
|
||
padding: var(--space-md) var(--space-lg);
|
||
background: color-mix(in srgb, var(--color-error) 10%, transparent);
|
||
border-bottom: 1px solid var(--border);
|
||
color: var(--color-error);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.terminal-error-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: var(--space-md, 12px);
|
||
}
|
||
|
||
.terminal-error-content span {
|
||
color: var(--color-error);
|
||
text-align: center;
|
||
}
|
||
|
||
.terminal-error-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
gap: var(--space-sm, 8px);
|
||
}
|
||
|
||
.terminal-retry-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-xs, 4px);
|
||
padding: 6px 12px;
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
color: var(--text);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
.terminal-retry-btn:hover {
|
||
background: var(--card-hover);
|
||
border-color: var(--text-muted);
|
||
}
|
||
|
||
/* === Terminal Modal Mobile Responsive === */
|
||
@media (max-width: 768px) {
|
||
.modal.terminal-modal {
|
||
/* Reset the desktop min-width: 480px / min-height: 320px — they pin the
|
||
modal at 480×320 on phones narrower than 480px and force horizontal
|
||
overflow. The fullscreen width/height below then take effect normally.
|
||
!important is required because useModalResizePersist writes inline
|
||
width/height from saved desktop sessions, and inline styles would
|
||
otherwise win over the media query. */
|
||
min-width: 0 !important;
|
||
min-height: 0 !important;
|
||
width: 100% !important;
|
||
width: 100vw !important;
|
||
max-width: 100vw !important;
|
||
min-height: 100dvh !important;
|
||
height: 100dvh !important;
|
||
max-height: 100dvh !important;
|
||
border-radius: 0;
|
||
border: none;
|
||
/* Disable user resize on mobile — fullscreen layout. */
|
||
resize: none;
|
||
}
|
||
|
||
/* Keep tabs and header actions on a single row */
|
||
.terminal-header {
|
||
flex-wrap: nowrap;
|
||
padding-top: env(safe-area-inset-top, 0);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Tabs stay scrollable and yield space to the fixed action cluster */
|
||
.terminal-tabs {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Hide the redundant title/status indicator on mobile — .terminal-status-bar shows connection state */
|
||
.terminal-title {
|
||
display: none;
|
||
}
|
||
|
||
/* Actions stay pinned to the right edge of the header row */
|
||
.terminal-actions {
|
||
flex: 0 0 auto;
|
||
justify-content: flex-end;
|
||
border-top: none;
|
||
padding: 0 var(--space-xs) 0 0;
|
||
min-height: 36px;
|
||
}
|
||
|
||
/* Hide text labels on action buttons to save space */
|
||
.terminal-action-label {
|
||
display: none;
|
||
}
|
||
|
||
.terminal-reconnect-btn,
|
||
.terminal-restart-btn,
|
||
.terminal-clear-btn {
|
||
padding: 8px;
|
||
min-height: 36px;
|
||
min-width: 36px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.terminal-tab {
|
||
min-height: 36px;
|
||
padding: 6px 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.terminal-tab-label {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.terminal-close {
|
||
width: 36px;
|
||
height: 36px;
|
||
}
|
||
|
||
.terminal-toolbar {
|
||
padding: 10px 12px;
|
||
padding-bottom: max(10px, env(safe-area-inset-bottom, 0));
|
||
}
|
||
|
||
.terminal-task-title {
|
||
font-size: 12px;
|
||
max-width: 200px;
|
||
}
|
||
|
||
.terminal-empty-state {
|
||
padding: var(--space-xl);
|
||
padding-bottom: max(24px, env(safe-area-inset-bottom, 0));
|
||
}
|
||
|
||
/* Mobile styles for interactive terminal */
|
||
.terminal-output {
|
||
padding: var(--space-md);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.terminal-welcome {
|
||
padding: 24px 16px;
|
||
}
|
||
|
||
.terminal-welcome h3 {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.terminal-commands-list {
|
||
gap: 6px;
|
||
}
|
||
|
||
.terminal-commands-list span {
|
||
padding: 3px 8px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.terminal-shortcuts {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.terminal-shortcut-panel {
|
||
max-height: calc(var(--space-2xl) + var(--space-2xl) + var(--space-2xl) + var(--space-xl));
|
||
}
|
||
|
||
.terminal-shortcut-btn,
|
||
.terminal-shortcut-btn--modifier {
|
||
min-width: calc(var(--space-xl) + var(--space-md));
|
||
min-height: calc(var(--space-xl) + var(--space-md));
|
||
font-size: 11px;
|
||
}
|
||
|
||
.terminal-preferences-panel {
|
||
grid-template-columns: 1fr;
|
||
max-height: calc(var(--space-2xl) * 8);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.terminal-preference-field--checkbox,
|
||
.terminal-preferences-reset {
|
||
align-self: stretch;
|
||
}
|
||
|
||
.terminal-font-size-btn {
|
||
min-width: calc(var(--space-xl) + var(--space-md));
|
||
min-height: calc(var(--space-xl) + var(--space-md));
|
||
}
|
||
|
||
.terminal-shortcuts kbd {
|
||
padding: 1px 4px;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.terminal-input-area {
|
||
padding: 10px 12px;
|
||
padding-bottom: max(10px, env(safe-area-inset-bottom, 0));
|
||
}
|
||
|
||
.terminal-input {
|
||
font-size: 16px;
|
||
padding: 6px 10px;
|
||
}
|
||
|
||
.terminal-kill-btn {
|
||
padding: 6px 12px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.terminal-output-text {
|
||
padding: 10px 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* When a virtual keyboard is open, constrain the modal to fit entirely
|
||
above the keyboard so the status bar / entry area stays visible.
|
||
--vv-height is the visualViewport.height in pixels (set by JS) and
|
||
works uniformly across Chrome Android and iOS Safari.
|
||
Falls back to 100dvh - overlap when --vv-height is not available. */
|
||
.terminal-modal[style*="--keyboard-overlap"] {
|
||
/* Neutralize inherited desktop min-height (90vh) so the modal can
|
||
shrink below the full-viewport mobile default when the keyboard
|
||
is open. Without this, min-height keeps the modal taller than
|
||
the available space and the bottom overlaps the keyboard. */
|
||
min-height: auto !important;
|
||
/* Apply both height and max-height so the modal is *exactly* the
|
||
visual viewport height — not just capped at it. Using height
|
||
ensures the element cannot be taller than max-height due to
|
||
inherited min-height or flex layout.
|
||
!important is required because the mobile fullscreen rule above
|
||
sets `height: 100dvh !important` to defeat persisted desktop
|
||
sizes from useModalResizePersist; without `!important` here, the
|
||
keyboard-aware shrink would lose the specificity battle and the
|
||
terminal would stay at 100dvh while the keyboard covered the
|
||
bottom of the viewport. */
|
||
height: var(--vv-height, calc(100dvh - var(--keyboard-overlap, 0px))) !important;
|
||
max-height: var(--vv-height, calc(100dvh - var(--keyboard-overlap, 0px))) !important;
|
||
/* Clip any content that exceeds the constrained height during
|
||
the keyboard-open transition so the terminal doesn't poke out
|
||
below the visible area while xterm re-fits. */
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Anchor the overlay to the top when the keyboard is open so the header
|
||
stays visible. Without this, align-items:center (from the non-keyboard
|
||
modal-overlay:has rule) vertically centers the shrunken modal and pushes
|
||
the top area out of the visible viewport. */
|
||
.modal-overlay:has(.terminal-modal[style*="--keyboard-overlap"]) {
|
||
align-items: flex-start !important;
|
||
}
|
||
|
||
/* Terminal tab close button: touch target on mobile */
|
||
.terminal-tab-close {
|
||
min-width: 36px;
|
||
min-height: 36px;
|
||
}
|
||
}
|