chore: rebuild CentOS7 release package

This commit is contained in:
shiyue
2026-06-25 21:52:09 +08:00
parent 04dd48deb2
commit c387c92e4b
11 changed files with 924 additions and 34 deletions

View File

@@ -2,7 +2,7 @@
(function () {
'use strict';
const ASSET_VERSION = '20260624-icon-refresh';
const ASSET_VERSION = '20260625-branch-bubble';
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
const RENDER_DEBOUNCE = 100;
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
@@ -11,10 +11,13 @@
const CROSS_CONVERSATION_REPLY_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-cross-replies';
const CROSS_CONVERSATION_REPLY_COLLAPSE_LIMIT = 500;
const ASSISTANT_LAST_SECTION_BUTTON_CLASS = 'msg-last-section-btn';
const ASSISTANT_BRANCH_BUTTON_CLASS = 'msg-branch-btn';
const ASSISTANT_LAST_SECTION_FOCUS_CLASS = 'msg-last-section-focus';
const ASSISTANT_LAST_SECTION_SCROLL_OFFSET = 72;
const ASSISTANT_LAST_SECTION_SKIP_SELECTOR = [
`.${ASSISTANT_LAST_SECTION_BUTTON_CLASS}`,
`.${ASSISTANT_BRANCH_BUTTON_CLASS}`,
'.msg-action-row',
'.msg-tools',
'.tool-call',
'.tool-group',
@@ -190,6 +193,7 @@
const attachmentPreviewCache = new Map();
let loginPasswordValue = ''; // store login password for force-change flow
let currentCwd = null;
let currentSessionMessageCount = 0;
let currentSessionRunning = false;
let fileBrowserState = null;
let directoryPickerState = null;
@@ -1406,17 +1410,74 @@
return button;
}
function getMessageActionRow(bubble) {
if (!bubble) return null;
let row = bubble.querySelector(':scope > .msg-action-row');
if (!row) {
row = document.createElement('div');
row.className = 'msg-action-row';
bubble.appendChild(row);
}
return row;
}
function syncAssistantLastSectionButton(messageEl) {
if (!messageEl?.classList?.contains('assistant')) return;
const bubble = messageEl.querySelector(':scope > .msg-bubble');
if (!bubble) return;
let button = bubble.querySelector(`:scope > .${ASSISTANT_LAST_SECTION_BUTTON_CLASS}`);
let button = bubble.querySelector(`:scope .${ASSISTANT_LAST_SECTION_BUTTON_CLASS}`);
const hasTarget = !!getAssistantLastSectionTarget(bubble);
if (!button && !hasTarget) return;
if (!button) button = createAssistantLastSectionButton();
button.hidden = !hasTarget;
button.disabled = !hasTarget;
bubble.appendChild(button);
getMessageActionRow(bubble)?.appendChild(button);
}
function createAssistantBranchButton(messageIndex) {
const button = document.createElement('button');
button.type = 'button';
button.className = ASSISTANT_BRANCH_BUTTON_CLASS;
button.title = '从这里分支新会话';
button.setAttribute('aria-label', '从这里分支新会话');
button.dataset.messageIndex = String(messageIndex);
button.innerHTML = `
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M6 3v12"></path>
<circle cx="6" cy="18" r="3"></circle>
<circle cx="18" cy="6" r="3"></circle>
<path d="M6 9h6a6 6 0 0 0 6-6"></path>
<path d="M15 18h6"></path>
<path d="M18 15v6"></path>
</svg>
`;
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const index = Number.parseInt(button.dataset.messageIndex || '', 10);
branchFromAssistantMessage(index);
});
return button;
}
function syncAssistantBranchButton(messageEl, messageIndex) {
if (!messageEl?.classList?.contains('assistant')) return;
if (!currentSessionId || !Number.isFinite(messageIndex) || messageIndex < 0) return;
const bubble = messageEl.querySelector(':scope > .msg-bubble');
if (!bubble) return;
let button = bubble.querySelector(`:scope .${ASSISTANT_BRANCH_BUTTON_CLASS}`);
if (!button) button = createAssistantBranchButton(messageIndex);
button.dataset.messageIndex = String(messageIndex);
getMessageActionRow(bubble)?.appendChild(button);
}
function markSessionMessageElement(messageEl, messageIndex) {
if (!messageEl || !Number.isFinite(messageIndex) || messageIndex < 0) return;
messageEl.dataset.sessionMessage = 'true';
messageEl.dataset.messageIndex = String(messageIndex);
if (messageEl.classList.contains('assistant')) {
syncAssistantBranchButton(messageEl, messageIndex);
}
}
function updateSessionIdBadge() {
@@ -1534,10 +1595,17 @@
function normalizeSessionSnapshot(payload, options = {}) {
const sessionId = payload.sessionId || payload.id || '';
const messages = cloneMessages(payload.messages || []);
const historyTotal = Number.isFinite(Number(payload.historyTotal))
? Math.max(0, Number(payload.historyTotal))
: messages.length;
const historyBaseIndex = Number.isFinite(Number(payload.historyBaseIndex))
? Math.max(0, Number(payload.historyBaseIndex))
: Math.max(0, historyTotal - messages.length);
return {
sessionId,
id: sessionId,
messages: cloneMessages(payload.messages || []),
messages,
title: payload.title || '新会话',
mode: payload.mode || 'yolo',
model: payload.model || '',
@@ -1558,6 +1626,8 @@
waitingReplyCount: Number(payload.waitingReplyCount || 0),
failedReplyCount: Number(payload.failedReplyCount || 0),
pendingReplies: Array.isArray(payload.pendingReplies) ? deepClone(payload.pendingReplies) : [],
historyTotal,
historyBaseIndex,
historyPending: !!payload.historyPending,
complete: options.complete !== undefined ? !!options.complete : !payload.historyPending,
};
@@ -3332,6 +3402,7 @@
closeFileBrowser();
currentSessionId = null;
loadedHistorySessionId = null;
currentSessionMessageCount = 0;
clearSessionLoading();
setCurrentSessionRunningState(false);
currentCwd = null;
@@ -3375,6 +3446,11 @@
}
currentSessionId = snapshot.sessionId;
loadedHistorySessionId = snapshot.sessionId;
currentSessionMessageCount = Math.max(
snapshot.historyTotal || 0,
snapshot.historyBaseIndex + (snapshot.messages || []).length,
(snapshot.messages || []).length,
);
setLastSessionForAgent(snapshot.agent, currentSessionId);
chatTitle.textContent = snapshot.title || '新会话';
updateSessionIdBadge();
@@ -3393,7 +3469,10 @@
}
currentModel = snapshot.model || '';
if (!preserveStreaming) {
renderMessages(snapshot.messages || [], { immediate: !!options.immediate });
renderMessages(snapshot.messages || [], {
immediate: !!options.immediate,
baseIndex: snapshot.historyBaseIndex || 0,
});
if (snapshot.isRunning && snapshot.sessionId === currentSessionId) {
startGenerating(snapshot.sessionId);
}
@@ -4231,6 +4310,7 @@
prependHistoryMessages(msg.messages || [], {
preserveScroll: !blocking,
skipScrollbar: blocking,
baseIndex: Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) : 0,
});
if (!msg.remaining) {
finalizeLoadedSession(msg.sessionId);
@@ -4266,11 +4346,13 @@
}
}
if (msg.sessionId === currentSessionId && msg.message) {
const messageIndex = currentSessionMessageCount;
currentSessionMessageCount += 1;
collectClosedCollabAgentIds([msg.message]).forEach((id) => closedCollabAgentIds.add(id));
const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove();
const shouldFollow = !(currentSessionRunning || isGenerating) || isNearBottom();
messagesDiv.appendChild(buildMsgElement(msg.message));
messagesDiv.appendChild(buildMsgElement(msg.message, messageIndex));
followOutputIfNeeded(shouldFollow);
setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning);
}
@@ -4494,6 +4576,10 @@
cwd: request.cwd,
agent: request.agent,
mode: request.mode,
model: request.model,
title: request.title,
branchSourceSessionId: request.branchSourceSessionId,
branchMessageIndex: request.branchMessageIndex,
createCwd: true,
requestId: request.requestId,
});
@@ -4503,6 +4589,10 @@
agent: request.agent,
cwd: request.rawCwd || request.cwd,
mode: request.mode,
model: request.model,
title: request.title,
branchSourceSessionId: request.branchSourceSessionId,
branchMessageIndex: request.branchMessageIndex,
});
},
});
@@ -4632,6 +4722,10 @@
function finishGenerating(sessionId) {
if (sessionId && currentSessionId && sessionId !== currentSessionId) return;
const hasPersistedAssistantMessage = !!(
pendingText
|| (Array.isArray(window.pendingContentBlocks) && window.pendingContentBlocks.length > 0)
);
isGenerating = false;
generatingSessionId = null;
updateNoteModeUI();
@@ -4674,6 +4768,11 @@
}
}
streamEl.removeAttribute('id');
if (hasPersistedAssistantMessage && currentSessionId) {
const messageIndex = currentSessionMessageCount;
currentSessionMessageCount += 1;
markSessionMessageElement(streamEl, messageIndex);
}
syncAssistantLastSectionButton(streamEl);
}
@@ -5769,7 +5868,7 @@
return fallbackMessages.length > 0 ? fallbackMessages[fallbackMessages.length - 1] : null;
}
function buildMsgElement(m) {
function buildMsgElement(m, messageIndex = null) {
const el = createMsgElement(m.role, m.content, m.attachments || [], m);
if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {
const bubble = el.querySelector('.msg-bubble');
@@ -5820,12 +5919,16 @@
}
}
}
if (Number.isFinite(messageIndex)) {
markSessionMessageElement(el, messageIndex);
}
return el;
}
function renderMessages(messages, options = {}) {
renderEpoch++;
const epoch = renderEpoch;
const baseIndex = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
closedCollabAgentIds = collectClosedCollabAgentIds(messages);
messagesDiv.innerHTML = '';
clearUserMessageIndex();
@@ -5838,7 +5941,7 @@
}
if (options.immediate) {
const frag = document.createDocumentFragment();
messages.forEach((message) => frag.appendChild(buildMsgElement(message)));
messages.forEach((message, index) => frag.appendChild(buildMsgElement(message, baseIndex + index)));
messagesDiv.appendChild(frag);
updateUserOutlinePanel();
renderPendingNotes({ scroll: false });
@@ -5861,7 +5964,7 @@
// Render first batch immediately
const frag0 = document.createDocumentFragment();
for (let i = batches[0][0]; i < batches[0][1]; i++) frag0.appendChild(buildMsgElement(messages[i]));
for (let i = batches[0][0]; i < batches[0][1]; i++) frag0.appendChild(buildMsgElement(messages[i], baseIndex + i));
messagesDiv.appendChild(frag0);
updateUserOutlinePanel();
renderPendingNotes({ scroll: false });
@@ -5878,7 +5981,7 @@
const prevHeight = messagesDiv.scrollHeight;
const prevScrollTop = messagesDiv.scrollTop;
const frag = document.createDocumentFragment();
for (let i = start; i < end; i++) frag.appendChild(buildMsgElement(messages[i]));
for (let i = start; i < end; i++) frag.appendChild(buildMsgElement(messages[i], baseIndex + i));
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
updateUserOutlinePanel();
// Compensate scrollTop so visible area stays unchanged
@@ -5890,13 +5993,14 @@
function prependHistoryMessages(messages, options = {}) {
if (!Array.isArray(messages) || messages.length === 0) return;
const baseIndex = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
collectClosedCollabAgentIds(messages).forEach((id) => closedCollabAgentIds.add(id));
const preserveScroll = options.preserveScroll !== false;
const skipScrollbar = options.skipScrollbar === true;
const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove();
const frag = document.createDocumentFragment();
messages.forEach((m) => frag.appendChild(buildMsgElement(m)));
messages.forEach((m, index) => frag.appendChild(buildMsgElement(m, baseIndex + index)));
if (!preserveScroll) {
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
updateUserOutlinePanel();
@@ -7210,6 +7314,11 @@
const messageId = createLocalId('user');
const element = createMsgElement('user', text, attachments, { messageId });
messagesDiv.appendChild(element);
if (currentSessionId) {
const messageIndex = currentSessionMessageCount;
currentSessionMessageCount += 1;
markSessionMessageElement(element, messageIndex);
}
registerUserMessage(messageId, element, text);
updateUserOutlinePanel();
scrollToBottom();
@@ -7259,6 +7368,11 @@
} else {
messagesDiv.appendChild(element);
}
if (currentSessionId) {
const messageIndex = currentSessionMessageCount;
currentSessionMessageCount += 1;
markSessionMessageElement(element, messageIndex);
}
registerUserMessage(messageId, element, text);
updateUserOutlinePanel();
if (shouldFollow) {
@@ -7886,6 +8000,31 @@
</select>
</div>
<div id="codex-profile-area"></div>
<div class="settings-divider"></div>
<div class="settings-section-title">容量失败重试</div>
<div class="settings-field">
<label>自动重试</label>
<select class="settings-select" id="codex-retry-mode">
<option value="limited">按次数重试</option>
<option value="forever">一直重试</option>
<option value="off">关闭</option>
</select>
</div>
<div class="settings-retry-grid">
<div class="settings-field">
<label>间隔(秒)</label>
<input type="number" id="codex-retry-interval" min="1" max="3600" step="1" inputmode="numeric">
</div>
<div class="settings-field" id="codex-retry-attempts-field">
<label>重试次数</label>
<input type="number" id="codex-retry-attempts" min="1" max="1000" step="1" inputmode="numeric">
</div>
</div>
<div class="settings-inline-note" id="codex-retry-note">
仅对没有产生文本或工具调用的 Codex 容量/过载失败生效,避免重复执行已有副作用的任务。
</div>
<div class="settings-actions">
<button class="btn-save" id="codex-save-btn">保存 Codex 配置</button>
</div>
@@ -7918,6 +8057,11 @@
const closeBtn = panel.querySelector('.settings-close');
const codexModeSelect = panel.querySelector('#codex-mode');
const codexProfileArea = panel.querySelector('#codex-profile-area');
const codexRetryModeSelect = panel.querySelector('#codex-retry-mode');
const codexRetryIntervalInput = panel.querySelector('#codex-retry-interval');
const codexRetryAttemptsInput = panel.querySelector('#codex-retry-attempts');
const codexRetryAttemptsField = panel.querySelector('#codex-retry-attempts-field');
const codexRetryNote = panel.querySelector('#codex-retry-note');
const codexStatus = panel.querySelector('#codex-status');
const codexSaveBtn = panel.querySelector('#codex-save-btn');
@@ -7928,6 +8072,7 @@
let currentCodexConfig = null;
let codexEditingProfiles = [];
let codexActiveProfile = '';
let codexRetryConfig = { mode: 'limited', intervalSeconds: 2, maxAttempts: 3 };
let _onUpdateInfo = null;
function showCodexStatus(msg, type) {
@@ -7935,6 +8080,42 @@
codexStatus.className = 'settings-status ' + (type || '');
}
function normalizeCodexRetryConfig(raw = {}) {
const mode = ['off', 'limited', 'forever'].includes(raw.mode) ? raw.mode : 'limited';
const intervalSeconds = Math.max(1, Math.min(3600, Number.parseInt(String(raw.intervalSeconds || ''), 10) || 2));
const maxAttempts = Math.max(1, Math.min(1000, Number.parseInt(String(raw.maxAttempts || ''), 10) || 3));
return { mode, intervalSeconds, maxAttempts };
}
function syncCodexRetryInputs() {
const mode = codexRetryModeSelect.value;
const disabled = mode === 'off';
codexRetryIntervalInput.disabled = disabled;
codexRetryAttemptsInput.disabled = disabled || mode === 'forever';
codexRetryAttemptsField.classList.toggle('settings-field-disabled', disabled || mode === 'forever');
codexRetryNote.textContent = mode === 'off'
? '已关闭自动重试;容量/过载失败会直接显示错误。'
: mode === 'forever'
? '会一直按固定间隔重试;仍只在没有文本或工具调用时触发。'
: '按指定次数重试;仍只在没有文本或工具调用时触发,避免重复执行副作用。';
}
function setCodexRetryConfig(config) {
codexRetryConfig = normalizeCodexRetryConfig(config);
codexRetryModeSelect.value = codexRetryConfig.mode;
codexRetryIntervalInput.value = String(codexRetryConfig.intervalSeconds);
codexRetryAttemptsInput.value = String(codexRetryConfig.maxAttempts);
syncCodexRetryInputs();
}
function readCodexRetryConfig() {
return normalizeCodexRetryConfig({
mode: codexRetryModeSelect.value,
intervalSeconds: codexRetryIntervalInput.value,
maxAttempts: codexRetryAttemptsInput.value,
});
}
function renderCodexProfileArea() {
const mode = codexModeSelect.value;
if (mode === 'local') {
@@ -8091,10 +8272,13 @@
codexModeSelect.value = currentCodexConfig.mode || 'local';
codexEditingProfiles = (currentCodexConfig.profiles || []).map((profile) => ({ ...profile }));
codexActiveProfile = currentCodexConfig.activeProfile || (codexEditingProfiles[0]?.name || '');
setCodexRetryConfig(currentCodexConfig.retry || codexRetryConfig);
renderCodexProfileArea();
};
codexModeSelect.addEventListener('change', renderCodexProfileArea);
codexRetryModeSelect.addEventListener('change', syncCodexRetryInputs);
setCodexRetryConfig(codexRetryConfig);
codexSaveBtn.addEventListener('click', () => {
if (codexModeSelect.value === 'custom' && codexEditingProfiles.length === 0) {
@@ -8106,6 +8290,7 @@
activeProfile: codexActiveProfile,
profiles: codexEditingProfiles,
enableSearch: false,
retry: readCodexRetryConfig(),
};
send({ type: 'save_codex_config', config });
showCodexStatus('已保存', 'success');
@@ -8680,21 +8865,59 @@
try { localStorage.setItem(RECENT_CWD_KEY, JSON.stringify(list)); } catch {}
}
function branchFromAssistantMessage(messageIndex) {
if (!currentSessionId) {
appendError('当前没有可分支的会话。', { transient: true, autoDismissMs: 4000 });
return;
}
if (!Number.isFinite(messageIndex) || messageIndex < 0) {
appendError('无法定位分支消息,请刷新会话后重试。', { transient: true, autoDismissMs: 4000 });
return;
}
const sourceTitle = (chatTitle.textContent || '新会话').trim() || '新会话';
const cwd = currentCwd || getSessionEffectiveCwd(currentSessionId) || null;
requestNewSession({
cwd,
rawCwd: cwd || '',
agent: currentAgent,
mode: currentMode,
model: currentModel || '',
title: `${sourceTitle} 的分支`,
branchSourceSessionId: currentSessionId,
branchMessageIndex: messageIndex,
});
}
function requestNewSession(options = {}) {
const cwd = options.cwd || null;
const rawCwd = options.rawCwd !== undefined ? options.rawCwd : (cwd || '');
const agent = normalizeAgent(options.agent || currentAgent);
const mode = ['default', 'plan', 'yolo'].includes(options.mode) ? options.mode : currentMode;
const model = typeof options.model === 'string' ? options.model.trim() : '';
const title = typeof options.title === 'string' ? options.title.trim() : '';
const branchSourceSessionId = String(options.branchSourceSessionId || '').trim();
const branchMessageIndex = Number.isFinite(Number(options.branchMessageIndex))
? Number(options.branchMessageIndex)
: null;
const requestId = createSessionSwitchRequestId('new');
pendingNewSessionRequest = {
cwd,
rawCwd,
agent,
mode,
model,
title,
branchSourceSessionId,
branchMessageIndex,
requestId,
};
if (cwd) saveRecentCwd(cwd);
send({ type: 'new_session', cwd, agent, mode, requestId });
const payload = { type: 'new_session', cwd, agent, mode, requestId };
if (model) payload.model = model;
if (title) payload.title = title;
if (branchSourceSessionId) payload.branchSourceSessionId = branchSourceSessionId;
if (branchMessageIndex !== null) payload.branchMessageIndex = branchMessageIndex;
send(payload);
}
// --- New Session Modal ---
@@ -8828,6 +9051,10 @@
rawCwd,
agent: targetAgent,
mode: requestedMode,
model: options.model || '',
title: options.title || '',
branchSourceSessionId: options.branchSourceSessionId || '',
branchMessageIndex: options.branchMessageIndex,
});
}

View File

@@ -20,7 +20,7 @@
document.documentElement.dataset.dividerTime = dividerTime;
})();
</script>
<link rel="stylesheet" href="style.css?v=20260624-icon-refresh">
<link rel="stylesheet" href="style.css?v=20260625-branch-bubble">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
</head>
<body>
@@ -169,6 +169,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js"></script>
<script src="app.js?v=20260624-icon-refresh"></script>
<script src="app.js?v=20260625-branch-bubble"></script>
</body>
</html>

View File

@@ -2376,11 +2376,22 @@ body.session-loading-active {
border-bottom-left-radius: 4px;
color: var(--text-primary);
}
.msg-last-section-btn {
.msg-action-row {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
margin-top: 8px;
}
.msg-action-row:empty {
display: none;
}
.msg-last-section-btn,
.msg-branch-btn {
appearance: none;
width: 28px;
height: 28px;
margin: 8px 0 0 auto;
margin: 0;
padding: 0;
display: flex;
align-items: center;
@@ -2393,21 +2404,25 @@ body.session-loading-active {
opacity: 0.72;
transition: opacity 0.15s ease, background 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.msg-last-section-btn[hidden] {
.msg-last-section-btn[hidden],
.msg-branch-btn[hidden] {
display: none;
}
.msg-last-section-btn svg {
.msg-last-section-btn svg,
.msg-branch-btn svg {
display: block;
flex-shrink: 0;
}
.msg-last-section-btn:hover {
.msg-last-section-btn:hover,
.msg-branch-btn:hover {
opacity: 1;
background: var(--bg-tertiary);
border-color: var(--accent);
color: var(--accent);
transform: translateY(-1px);
}
.msg-last-section-btn:focus-visible {
.msg-last-section-btn:focus-visible,
.msg-branch-btn:focus-visible {
opacity: 1;
outline: 2px solid rgba(91, 126, 161, 0.28);
outline-offset: 2px;
@@ -4167,6 +4182,19 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
}
.settings-field input:focus,
.settings-select:focus { border-color: var(--accent); }
.settings-field input:disabled,
.settings-select:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.settings-field-disabled {
opacity: 0.65;
}
.settings-retry-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 12px;
}
.settings-select {
-webkit-appearance: none;
appearance: none;
@@ -4219,6 +4247,7 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
.settings-panel { width: 95%; padding: 20px 16px; }
.settings-nav-card { padding: 13px 14px; }
.settings-back { width: 32px; height: 32px; }
.settings-retry-grid { grid-template-columns: 1fr; }
}
/* === Force Change Password Overlay === */