	/* ============================================================
	TOKENS
	============================================================ */

	@font-face {
		font-display: swap;
		font-family: "Reem Kufi";
		font-style: normal;
		font-weight: 400;
		src: url("/reem-kufi-v28-latin-regular.20aef0.woff2") format("woff2")
	}

	@font-face {
		font-display: swap;
		font-family: "Reem Kufi";
		font-style: normal;
		font-weight: 500;
		src: url("/reem-kufi-v28-latin-500.a35370.woff2") format("woff2")
	}

	@font-face {
		font-display: swap;
		font-family: "Reem Kufi";
		font-style: normal;
		font-weight: 700;
		src: url("/reem-kufi-v28-latin-700.60a748.woff2") format("woff2")
	}

	/* Theming is driven by html[data-theme]; applyTheme() in main.js sets it.
	Each *-wash is its base colour at low alpha — keep the pairs in step. */
	:root {
		color-scheme: dark;
		--bg: #060A3A;
		--surface: #0A0F45;
		--surface-2: #1c2054;
		--side-panel: #0A0F45;
		--surface-hover: #262a63;
		--surface-hover-2: #313676;
		--border: #2A2F6B;
		--text: #E7ECEA;
		--text-muted: #cbcce2;
		--text-faint: #656B9E;
		--accent: #b280ef;
		--accent-strong: #7B4FFA;
		--accent-wash: rgba(123, 79, 250, 0.16);
		--accent-glow: rgba(123, 79, 250, 0.45);
		--on-accent: #0D1210;
		/* text sitting on an --accent fill */
		--warn: #E8A23D;
		--warn-wash: rgba(232, 162, 61, 0.14);
		--warn-glow: rgba(232, 162, 61, 0.45);
		--danger: #E2604F;
		--danger-wash: rgba(226, 96, 79, 0.12);
		--radius-sm: 6px;
		--radius-md: 10px;
		--radius-lg: 16px;
		--font-mono: 'IBM Plex Mono', ui-monospace, monospace;
		--font-body: "Reem Kufi", "Gill Sans MT", Calibri, "Trebuchet MS", sans-serif;
		--rail-w: 81px;
		/* icon rail — column 1 of #app */
		--config-w: 300px;
		/* settings + history — column 2 of #app */
	}

	html[data-theme="light"] {
		color-scheme: light;
		--bg: #eae6f0;
		--surface: #FFFFFF;
		--surface-2: #f5f3ff;
		--side-panel: #eae6f0;
		--surface-hover: #E2DBF5;
		--surface-hover-2: #D6CCF0;
		--border: #cfcadc;
		--text: #151321;
		--text-muted: #5a5560;
		--text-faint: #968fa1;
		--accent: #7B00D4;
		--accent-strong: #FF85EA;
		--accent-wash: rgba(90, 47, 204, 0.10);
		--accent-glow: rgba(90, 47, 204, 0.32);
		--on-accent: #FFFFFF;
		--warn: #C97F1F;
		--warn-wash: rgba(201, 127, 31, 0.12);
		--warn-glow: rgba(201, 127, 31, 0.40);
		--danger: #C24632;
		--danger-wash: rgba(194, 70, 50, 0.10);
	}

	* {
		box-sizing: border-box;
	}

	html,
	body {
		height: 100%;
	}

	body {
		margin: 0;
		background: var(--bg);
		color: var(--text);
		font-family: var(--font-body);
		font-size: 16px;
		line-height: 1.5;
		-webkit-font-smoothing: antialiased;
	}

	::selection {
		background: var(--accent-wash);
		color: var(--text);
	}

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

	button,
	input,
	select,
	textarea {
		font-family: inherit;
		font-size: inherit;
		line-height: inherit;
		color: inherit;
	}

	button {
		cursor: pointer;
	}

	a {
		color: inherit;
	}

	hr {
		border: none;
		border-top: 1px solid var(--border);
	}

	/* ============================================================
	APP SHELL
	Three columns: icon rail | config pane | chat. Pinned to the
	viewport with overflow hidden; only #history-list, #log and
	#history-chat scroll.
	============================================================ */
	#app {
		display: grid;
		grid-template-columns: var(--rail-w) var(--config-w) 1fr;
		height: 100vh;
		height: 100dvh;
		overflow: hidden;
	}

	/* Dims the chat behind the off-canvas sidebars on mobile; click to dismiss */
	#sidebar-overlay {
		display: none;
		position: fixed;
		inset: 0;
		background: rgba(0, 0, 0, 0.5);
		z-index: 20;
	}

	/* ---------- RAIL ---------- */
	#rail {
		grid-column: 1;
		background: var(--side-panel);
		border-right: 1px solid var(--border);
		display: flex;
		flex-direction: column;
		align-items: center;
		padding: 16px 0px;
		gap: 4px;
		z-index: 5;
		/* Without these, the rail's own button stack (no min-height:0, and nothing
		to scroll it) sets the min-content size #app's implicit grid row is forced
		to honour — on a short viewport that inflates the row past the viewport
		height, and #config-pane/#main-content get stretched along with it, clipped
		by #app's overflow:hidden with no way to scroll back to what's clipped. */
		min-height: 0;
		overflow-y: auto;
	}

	#rail .rail-logo {
		width: 60px;
		height: 50px;
		display: flex;
		align-items: center;
		justify-content: center;
		margin-bottom: 18px;
	}

	#rail .rail-logo img {
		width: 60px;
		height: 50px;
		display: block;
	}

	html[data-theme="light"] #rail .rail-logo img {
		filter: invert(1);
	}

	/* Pushes theme/support/logout to the bottom of the rail */
	.rail-spacer {
		flex: 1;
	}

	.rail-divider {
		width: 32px;
		height: 1px;
		background: var(--border);
		margin: 6px 0 2px;
	}

	/* .active is set by activateMode() and drives the left accent bar */
	.mode-btn {
		width: 80px;
		background: none;
		border: none;
		border-left: 2px solid transparent;
		border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
		padding: 9px 4px 10px;
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 5px;
		color: var(--text-muted);
		transition: background .15s ease, color .15s ease;
	}

	.mode-btn svg {
		width: 20px;
		height: 20px;
	}

	.mode-btn span {
		font-size: 11px;
		letter-spacing: .03em;
		text-transform: uppercase;
	}

	.mode-btn:hover {
		background: var(--surface-hover);
		color: var(--text);
	}

	.mode-btn.active {
		color: var(--text);
		background: var(--accent-wash);
		border-left-color: var(--accent);
	}

	/* These hold inline SVGs — nothing may overwrite their textContent */
	.rail-utility {
		width: 36px;
		height: 36px;
		display: flex;
		align-items: center;
		justify-content: center;
		border: none;
		background: none;
		border-radius: var(--radius-sm);
		color: var(--text-muted);
		margin-top: 2px;
	}

	.rail-utility:hover {
		background: var(--surface-hover);
		color: var(--text);
	}

	.rail-utility svg {
		width: 17px;
		height: 17px;
	}

	/* ---------- CONFIG PANE ----------
	Fixed settings block on top, history below. min-height:0 here is what lets
	the pane shrink to the grid row's height instead of growing it — #history-section's
	own min-height/max-height (below) decide, via ordinary flex clamping, whether
	the leftover space fits it (no pane scrollbar, just its own list scrolls) or
	not (the pane scrolls instead, with history capped rather than squeezed thin). */
	#config-pane {
		grid-column: 2;
		background: var(--side-panel);
		border-right: 1px solid var(--border);
		display: flex;
		flex-direction: column;
		min-height: 0;
		overflow-y: auto;
	}

	/* Help has no settings, so hide the config pane to give the help text more space */
	#app:has(#helpBtn.active) #config-pane {
		display: none;
	}

	#app:has(#helpBtn.active) #main-content {
		grid-column: 2 / 4;
	}


	#config-section {
		padding: 18px 16px 16px;
		border-bottom: 1px solid var(--border);
		flex-shrink: 0;
	}

	.config-header {
		font-family: var(--font-mono);
		letter-spacing: .08em;
		text-transform: uppercase;
		color: var(--text-faint);
		margin: 0 0 12px;
		display: flex;
		align-items: center;
		justify-content: space-between;
	}

	.newChatBtn {
		width: 100%;
		background: linear-gradient(135deg, var(--accent-strong), var(--accent));
		color: #fff;
		border: 1px solid transparent;
		border-radius: var(--radius-sm);
		padding: 8px 10px;
		font-weight: 600;
		margin-bottom: 16px;
		box-shadow: 0 3px 14px var(--accent-glow);
		transition: box-shadow .15s ease, filter .15s ease, transform .1s ease;
	}

	.newChatBtn:hover {
		filter: brightness(1.08);
		box-shadow: 0 4px 20px var(--accent-glow);
	}

	.newChatBtn:active {
		transform: scale(0.98);
	}

	/* make the new chat button glow gently when viewing history without a live chat */
	.pulse,
	#app:not(.chat-live):has(#history-chat.visible) #newChatBtn {
		animation: pulse-attention 1.8s ease-in-out infinite;
	}

	@keyframes pulse-attention {

		0%,
		100% {
			box-shadow: 0 3px 14px var(--accent-glow);
		}

		50% {
			box-shadow: 0 3px 22px var(--accent-glow), 0 0 0 3px var(--accent-wash);
		}
	}

	/* No display of its own, so the `hidden` attribute works directly — that's how
	the LLM model and system prompt disappear in Transcribe mode, and how the
	ASR settings disappear in Text Chat. */
	.field-group {
		margin-bottom: 14px;
	}

	.field-group:last-child {
		margin-bottom: 0;
	}

	.field-label {
		display: block;
		font-weight: 600;
		color: var(--text-muted);
		margin-bottom: 6px;
	}

	input#silenceTimeout,
	input#keywordBoosting {
		width: 100%;
		background: var(--surface-2);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-sm);
		padding: 8px 9px;
		font-family: var(--font-mono);
		resize: vertical;
	}

	input#silenceTimeout:focus,
	input#keywordBoosting:focus {
		border-color: var(--accent);
	}

	/* .invalid is applied by validateTimeout() in main.js */
	input.invalid {
		border-color: var(--danger) !important;
		background: var(--danger-wash);
	}

	.input-error {
		color: var(--danger);
		font-size: 14px;
		margin-top: 4px;
		display: block;
	}

	.input-hint {
		color: var(--text-faint);
		font-size: 14px;
		margin-top: 5px;
		display: block;
		line-height: 1.4;
	}

	/* display:block above beats the UA's [hidden]{display:none}, so without this
	the error text is permanently on screen and `.hidden = true` does nothing. */
	.input-error[hidden] {
		display: none;
	}

	.label-row {
		position: relative;
		width: max-content;
		max-width: 100%;
	}

	/* No underline — five dotted lines down a 300px pane reads as broken text.
	cursor:help plus a colour lift on hover is enough of a hint. */
	.label-row .field-label {
		cursor: help;
		transition: color .15s ease;
	}

	.label-row:hover .field-label,
	.label-row:focus-within .field-label {
		color: var(--text);
	}

	.tip {
		position: absolute;
		top: 100%;
		left: 0;
		z-index: 60;
		width: max-content;
		max-width: 236px;
		/* --config-w less 16px padding each side, less a margin */
		background: var(--surface);
		color: var(--text-muted);
		border: 1px solid var(--border);
		border-radius: var(--radius-sm);
		box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
		padding: 8px 10px;
		font-size: 14px;
		font-weight: 400;
		line-height: 1.45;
		opacity: 0;
		visibility: hidden;
		transform: translateY(-2px);
		pointer-events: none;
		/* never swallows a click aimed at the input beneath */
		transition: opacity .12s ease, transform .12s ease, visibility 0s linear .12s;
	}

	/* Delay on the way in, none on the way out — otherwise they flicker as the
	pointer sweeps down the pane. */
	.label-row:hover .tip,
	.label-row:focus-within .tip {
		opacity: 1;
		visibility: visible;
		transform: translateY(0);
		transition: opacity .12s ease .25s, transform .12s ease .25s, visibility 0s;
	}

	.advanced-group {
		margin-top: 4px;
		padding-top: 12px;
		border-top: 1px solid var(--border);
	}

	/* details carries its own display, so [hidden] needs the override — same trap
	as .input-error above and #stopRecording below. */
	.advanced-group[hidden] {
		display: none;
	}

	.advanced-group>summary {
		display: flex;
		align-items: center;
		gap: 6px;
		list-style: none;
		/* kills the default triangle */
		cursor: pointer;
		font-family: var(--font-mono);
		letter-spacing: .08em;
		text-transform: uppercase;
		color: var(--text-faint);
		padding: 2px 0;
		transition: color .15s ease;
	}

	.advanced-group>summary::-webkit-details-marker {
		display: none;
	}

	.advanced-group>summary:hover {
		color: var(--text-muted);
	}

	.advanced-group>summary svg {
		width: 12px;
		height: 12px;
		flex-shrink: 0;
		transition: transform .15s ease;
	}

	.advanced-group[open]>summary svg {
		transform: rotate(90deg);
	}

	.advanced-body {
		padding-top: 12px;
	}

	/* Keywords are baked into the websocket URL at connect time, so the field is
	disabled mid-session by updateButtonVisibility() — same reason the language
	picker is locked (see the #app.chat-live rules). The silence timeout is
	polled live, so it stays live. */
	input#keywordBoosting:disabled {
		opacity: .5;
		cursor: default;
		border-color: var(--border);
	}

	/* ---------- CUSTOM TOOLS ---------- */
	.tools-list {
		list-style: none;
		margin: 0 0 10px;
		padding: 0;
		display: flex;
		flex-direction: column;
		gap: 10px;
	}

	.tool-item {
		background: var(--surface-2);
		border: 1px solid var(--border);
		border-radius: var(--radius-sm);
		padding: 9px;
		display: flex;
		flex-direction: column;
		gap: 8px;
	}

	.tool-item-header {
		display: flex;
		align-items: center;
		gap: 6px;
	}

	.tool-name-input {
		flex: 1;
		min-width: 0;
		background: var(--surface);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-sm);
		padding: 7px 9px;
		font-family: var(--font-mono);
		font-weight: 600;
	}

	.tool-name-input:focus,
	.tool-description-input:focus {
		border-color: var(--accent);
	}

	.tool-description-input {
		width: 100%;
		background: var(--surface);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-sm);
		padding: 7px 9px;
		font-family: inherit;
		font-size: 14px;
		resize: vertical;
	}

	.tool-output-toggle {
		font-size: 14px;
		color: var(--text-muted);
	}

	.tool-arguments {
		border-top: 1px solid var(--border);
		padding-top: 8px;
	}

	.tool-arguments-label {
		font-size: 12px;
		font-family: var(--font-mono);
		letter-spacing: .08em;
		text-transform: uppercase;
		color: var(--text-faint);
		margin-bottom: 6px;
	}

	.tool-arguments-list {
		list-style: none;
		margin: 0 0 6px;
		padding: 0;
		display: flex;
		flex-direction: column;
		gap: 5px;
	}

	.tool-argument-item {
		display: flex;
		align-items: center;
		gap: 5px;
	}

	.tool-argument-name-input,
	.tool-argument-description-input {
		min-width: 0;
		background: var(--surface);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-sm);
		padding: 6px 8px;
		font-size: 13px;
	}

	.tool-argument-name-input {
		flex: 0 1 40%;
		font-family: var(--font-mono);
	}

	.tool-argument-description-input {
		flex: 1 1 60%;
	}

	.tool-argument-name-input:focus,
	.tool-argument-description-input:focus {
		border-color: var(--accent);
	}

	/* Tool/argument rows are small and few, so their delete buttons stay
	visible rather than fading in on hover like .history-item's — nothing
	forces a hover pass over a config row the way a scannable history list
	does. */
	.tool-item .delete-btn,
	.tool-argument-item .delete-btn {
		opacity: .6;
	}

	.tool-item .delete-btn:hover,
	.tool-argument-item .delete-btn:hover {
		opacity: 1;
	}

	.add-tool-btn,
	.add-argument-btn {
		width: 100%;
		background: var(--surface-2);
		border: 1px dashed var(--border);
		color: var(--text-muted);
		border-radius: var(--radius-sm);
		padding: 7px 9px;
		font-size: 14px;
	}

	.add-argument-btn {
		background: var(--surface);
		font-size: 13px;
		padding: 6px 8px;
	}

	.add-tool-btn:hover,
	.add-argument-btn:hover {
		color: var(--text);
		border-color: var(--accent);
	}

	/* ---------- CUSTOM DROPDOWN ----------
	The real <select> stays in the DOM (hidden, untabbable) so .value and
	'change' listeners keep working; everything visible is built by
	enhanceSelect(). Code that sets .value must also dispatch 'change'. */
	.native-select {
		position: absolute;
		opacity: 0;
		width: 1px;
		height: 1px;
		pointer-events: none;
	}

	.custom-select {
		position: relative;
		/* anchor for the absolutely-positioned panel */
	}

	.cs-trigger {
		width: 100%;
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 8px;
		background: var(--surface-2);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-sm);
		padding: 8px 9px;
		font-family: var(--font-mono);
		text-align: left;
		cursor: pointer;
		transition: border-color .15s ease, background-color .15s ease;
	}

	.cs-trigger:hover {
		border-color: var(--accent);
		background: var(--surface-hover);
	}

	.cs-label {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}

	.cs-trigger svg {
		width: 14px;
		height: 14px;
		flex-shrink: 0;
		color: var(--text-muted);
		transition: transform .15s ease;
	}

	/* .open lives on the wrapper and drives both the chevron flip and the panel */
	.custom-select.open .cs-trigger {
		border-color: var(--accent);
	}

	.custom-select.open .cs-trigger svg {
		transform: rotate(180deg);
	}

	.cs-panel {
		display: none;
		position: absolute;
		left: 0;
		right: 0;
		top: calc(100% + 4px);
		background: var(--surface);
		border: 1px solid var(--border);
		border-radius: var(--radius-sm);
		box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
		max-height: 220px;
		overflow-y: auto;
		z-index: 50;
		padding: 4px;
	}

	.custom-select.open .cs-panel {
		display: block;
	}

	/* .selected = the committed value; .focused = keyboard cursor while arrowing */
	.cs-option {
		padding: 7px 9px;
		border-radius: var(--radius-sm);
		font-family: var(--font-mono);
		color: var(--text);
		cursor: pointer;
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
	}

	.cs-option:hover,
	.cs-option.focused {
		background: var(--surface-hover);
	}

	.cs-option.selected {
		background: var(--accent-wash);
		color: var(--accent);
	}

	/* ---------- FREE-TEXT VARIANT ----------
	data-freetext on the <select> swaps the button trigger for an input, so the
	listed options become suggestions rather than the only choices. Same box as
	.cs-trigger; the chevron becomes a tabindex=-1 button beside it. */
	.cs-combo {
		display: flex;
		align-items: center;
		gap: 4px;
		background: var(--surface-2);
		border: 1px solid var(--border);
		border-radius: var(--radius-sm);
		padding: 0 5px 0 9px;
		transition: border-color .15s ease, background-color .15s ease;
	}

	.cs-combo:hover {
		border-color: var(--accent);
	}

	.custom-select.open .cs-combo {
		border-color: var(--accent);
	}

	.cs-input {
		flex: 1;
		min-width: 0;
		background: none;
		border: none;
		color: var(--text);
		font-family: var(--font-mono);
		padding: 8px 0;
	}

	.cs-input:focus {
		outline: none;
	}

	.cs-input:disabled {
		cursor: default;
	}

	.cs-input::placeholder {
		color: var(--text-faint);
	}

	.cs-caret {
		display: flex;
		align-items: center;
		background: none;
		border: none;
		padding: 2px;
		color: var(--text-muted);
		flex-shrink: 0;
	}

	.cs-caret svg {
		width: 14px;
		height: 14px;
		transition: transform .15s ease;
	}

	.custom-select.open .cs-caret svg {
		transform: rotate(180deg);
	}

	/* Shown when nothing in the panel matches what's been typed */
	.cs-empty {
		padding: 7px 9px;
		font-family: var(--font-mono);
		font-size: 14px;
		line-height: 1.4;
		color: var(--text);
	}

	/* The language picker sits inline in the "English → X" pill, so its trigger
	drops the border and background it has elsewhere. */
	.lang-select-wrap {
		display: flex;
		align-items: center;
		gap: 8px;
		background: var(--surface-2);
		border: 1px solid var(--border);
		border-radius: var(--radius-sm);
		padding: 6px 10px;
	}

	.lang-fixed {
		color: var(--text-muted);
		white-space: nowrap;
	}

	.lang-arrow {
		color: var(--text-faint);
	}

	.lang-select-wrap .custom-select {
		flex: 1;
		min-width: 130px;
	}

	.lang-select-wrap .cs-trigger {
		background: none;
		border: none;
		padding: 2px 4px;
	}

	.lang-select-wrap .cs-trigger:hover {
		background: var(--surface-hover);
		border-radius: var(--radius-sm);
	}

	/* Same flattening for the free-text variant */
	.lang-select-wrap .cs-combo {
		background: none;
		border: none;
		padding: 0 2px;
	}

	.lang-select-wrap .cs-input {
		padding: 2px;
	}

	/* Language/model/ASR-language pickers are baked into the websocket URL at
	connect time, so changing any of them mid-session would silently do nothing —
	lock them while #app.chat-live. pointer-events:none doesn't stop typing though,
	so updateButtonVisibility() (main.js) still disables the free-text input for
	real on top of this. */
	#app.chat-live .custom-select:has(#targetLanguage, #llmModelSelect, #asrModelSelect, #asrLangSelect) {
		opacity: .5;
		pointer-events: none;
	}

	#app.chat-live .custom-select:has(#targetLanguage, #llmModelSelect, #asrModelSelect, #asrLangSelect) .cs-trigger {
		cursor: default;
	}

	/* ---------- HISTORY ----------
	flex:1 claims whatever space is left under #config-section. The clamp on
	either side is what makes #config-pane's scrollbar (see above) conditional
	without any JS or media query: given enough room this settles somewhere
	between the two and the pane never overflows; on a short viewport min-height
	wins over the shrink, the pane overflows instead, and max-height stops this
	from claiming the entire scrollable pane just because it now can. */
	#history-section {
		flex: 1;
		min-height: 250px;
		max-height: 70%;
		display: flex;
		flex-direction: column;
		padding: 16px 16px 12px;
		overflow: hidden;
	}

	.tooltip-icon {
		cursor: help;
		opacity: .7;
	}

	/* Only shown while an archived chat is open AND a session is still live
	underneath it, to get back to that live one. Stays [hidden] otherwise —
	see #refreshNotice above for why the id selector below wins regardless. */
	.show-active-chat-btn {
		width: 100%;
		background: var(--surface-2);
		border: 1px dashed var(--border);
		color: var(--text-muted);
		border-radius: var(--radius-sm);
		padding: 7px 9px;
		margin-bottom: 8px;
	}

	#app.chat-live:has(#history-chat.visible) #showActiveChatBtn {
		display: block;
	}

	.show-active-chat-btn:hover {
		color: var(--text);
		border-color: var(--accent);
	}

	#history-list {
		list-style: none;
		margin: 0;
		padding: 0;
		overflow-y: auto;
		flex: 1;
		display: flex;
		flex-direction: column;
		gap: 3px;
	}

	.history-item {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 6px;
		padding: 8px 9px;
		border-radius: var(--radius-sm);
		font-size: 14px;
		color: var(--text-muted);
		overflow: hidden;
		cursor: pointer;
	}

	.history-item:hover {
		background: var(--surface-2);
		color: var(--text);
	}

	/* .active = the archive currently open in #history-chat */
	.history-item.active {
		background: var(--accent-wash);
		color: var(--accent);
	}

	/* Added just before the row is spliced out, so deletes aren't a jump cut —
	deleteHistoryEntry() waits on animationend before touching localStorage. */
	.history-item.deleting {
		animation: history-item-delete 0.18s ease forwards;
		pointer-events: none;
	}

	@keyframes history-item-delete {
		from {
			opacity: 1;
			max-height: 60px;
		}

		to {
			opacity: 0;
			transform: translateX(10px);
			max-height: 0;
			padding-top: 0;
			padding-bottom: 0;
			margin: 0;
		}
	}

	.history-text {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
		flex: 1;
	}

	/* Hidden until the row is hovered or the button itself takes focus */
	.delete-btn {
		background: none;
		border: none;
		padding: 2px;
		opacity: 0;
		flex-shrink: 0;
		border-radius: 4px;
	}

	.history-item:hover .delete-btn,
	.delete-btn:focus-visible {
		opacity: .6;
	}

	.delete-btn:hover {
		opacity: 1 !important;
		background: var(--danger-wash);
	}

	.delete-btn img {
		display: block;
		width: 14px;
		height: 14px;
	}

	/* ---------- MAIN / CHAT ---------- */
	#main-content {
		grid-column: 3;
		display: flex;
		flex-direction: column;
		min-width: 0;
		min-height: 0;
		position: relative;
		/* containing block for the mic FAB */
		background: var(--bg);
	}

	#chat-header {
		flex-shrink: 0;
		padding: 16px 28px;
		border-bottom: 1px solid var(--border);
	}

	#chat-header h2 {
		font-size: 18px;
		margin: 0 0 2px;
		font-weight: 600;
	}

	#chat-header p {
		margin: 0;
		font-size: 14px;
		color: var(--text-muted);
	}

	/* ---------- AI DISCLAIMER ----------
	Shown in every mode where an LLM writes into the log (modeMeta.llm) and
	pinned in #chat-header rather than #log, so scrolling can't take it away
	and it stands whether you're in the live chat or an archive.
	activateMode() owns `hidden` — nothing else may touch it.
	A <div>, not a <p>: `#chat-header p` is (1,0,1) and would outrank the
	id selector below on margin/font-size/colour. */
	#aiDisclaimer {
		display: flex;
		align-items: flex-start;
		gap: 7px;
		font-size: 14px;
		margin: 10px 0 0;
		padding: 7px 10px;
		border: 1px solid var(--border);
		border-radius: var(--radius-sm);
		color: var(--text-muted);
		line-height: 1.45;
		max-width: 760px;
	}

	/* display:flex above beats the UA's [hidden]{display:none} — same trap as
	.input-error and #stopRecording. */
	#aiDisclaimer[hidden] {
		display: none;
	}

	#aiDisclaimer svg {
		width: 14px;
		height: 14px;
		flex-shrink: 0;
		margin-top: 2px;
		color: var(--warn);
	}

	/* Replaces the chat area entirely while Help is selected */
	#help-panel {
		max-width: 750px;
		margin: auto;
		padding: 32px;
		color: var(--text-muted);
		overflow-y: auto;
		height: 100%;
	}

	#help-panel p:first-child {
		color: var(--text);
		font-size: 20px;
		font-weight: 600;
		margin-top: 0;
	}

	#help-panel ul {
		padding-left: 18px;
	}

	#help-panel li {
		margin-bottom: 4px;
	}

	#help-panel p strong {
		color: var(--text);
	}

	/* Shown only when there's no live connection, you're not reading an archive,
	and Help isn't open — #app.chat-live is set by updateButtonVisibility()
	(main.js), #history-chat.visible by showActiveChat()/hideActiveChat(),
	#helpBtn.active by setActiveButton(). Which of #startButtons / #textChatStart
	is inside it is owned by activateMode(). */
	#chat-controls {
		flex-shrink: 0;
		padding: 14px 28px;
		border-bottom: 1px solid var(--border);
		position: absolute;
		top: 45%;
		align-self: center;
		z-index: 1;
	}

	#app.chat-live #chat-controls,
	#app:has(#history-chat.visible) #chat-controls,
	#app:has(#helpBtn.active) #chat-controls {
		display: none;
	}

	/* Shown from the moment a start button connects until the socket's first
	message comes back (see connect_to_hub / ws.onmessage in main.js) — the
	round trip to the hub has no other visible feedback. Same centring trick
	as #chat-controls, which it replaces on screen. */
	#connectThrobber {
		position: absolute;
		top: 45%;
		align-self: center;
		z-index: 1;
		width: 28px;
		height: 28px;
		border-radius: 50%;
		border: 3px solid var(--border);
		border-top-color: var(--accent);
		animation: spin .8s linear infinite;
	}

	@keyframes spin {
		to {
			transform: rotate(360deg);
		}
	}

	#chat-controls div div:first-of-type button:first-of-type,
	#chat-controls div input:first-of-type {
		background: linear-gradient(var(--rotate), var(--accent-strong), var(--accent));
		animation: sparkle linear 5s infinite;
	}

	@property --rotate {
		syntax: '<angle>';
		initial-value: 0deg;
		inherits: false;
	}

	@keyframes sparkle {
		from {
			--rotate: 180deg;
		}

		to {
			--rotate: 540deg;
		}
	}

	#startButtons>div {
		display: flex;
		gap: 8px;
		flex-wrap: wrap;
	}

	.startButton {
		background: var(--surface-2);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-sm);
		padding: 8px 14px;
		font-weight: 500;
		transition: border-color .15s ease, background .15s ease;
	}

	.startButton:hover {
		border-color: var(--accent);
		background: var(--surface-hover-2);
	}

	/* Shown only while a connection is live and you're not reading an archive.
	Stays [hidden] in the markup; #app.chat-live overrides it because an id
	selector always outranks a bare attribute selector. */
	#refreshNotice {
		padding: 10px 28px 0;
		flex-shrink: 0;
	}

	#app.chat-live:not(:has(#history-chat.visible)) #refreshNotice {
		display: block;
	}

	#refreshNotice p {
		margin: 0 0 6px;
		font-size: 14px;
		color: var(--text-faint);
	}

	#refreshNotice hr {
		margin-top: 10px;
	}

	/* ---------- CHAT LOG ----------
	#log is the live conversation, #history-chat renders a saved one.
	Exactly one carries .visible; showActiveChat()/hideActiveChat() swap them. */
	#log,
	.history-chat {
		flex: 1;
		min-height: 0;
		overflow-y: auto;
		display: none;
		flex-direction: column;
		gap: 14px;
		/* spacing between bubbles */
	}

	#log {
		padding: 24px 28px 100px;
		margin-bottom: 50px;
	}

	/* 100px clears the mic FAB (60px + 26px) */
	.history-chat {
		padding: 24px 28px 40px;
	}

	/* no FAB over the archive view */
	#log.visible,
	.history-chat.visible {
		display: flex;
	}

	/* Text chat has a composer under the log instead of the FAB, so the clearance
	above is just dead space. */
	#log:has(+ #textChatInput:not([hidden])) {
		padding-bottom: 24px;
	}

	/* Turn separator cloned from #hr-template. Deliberately invisible — it exists
	only for the extra margin between a user turn and the reply. */
	#log hr,
	.history-chat hr {
		border: none;
		margin: 6px 0;
		opacity: 0;
		height: 0;
	}

	/* User bubble — right-aligned, accent fill, tail bottom-right.
	Two rows: the label, then a body row holding the committed transcript and
	the interim hypothesis side by side so they read as one sentence. */
	.user_msg {
		align-self: flex-end;
		max-width: min(560px, 78%);
		background: var(--accent);
		color: var(--on-accent);
		border-radius: var(--radius-lg) var(--radius-lg) 4px var(--radius-lg);
		padding: 10px 14px;
		line-height: 1.5;
		display: flex;
		flex-direction: column;
		gap: 2px;
	}

	/* Faded so you can see which words the ASR hasn't committed to yet */
	.user_msg_provisional {
		opacity: .65;
	}

	/* Assistant bubble — bare <pre> children of the log, mirrored to the left.
	pre-wrap keeps the model's own newlines without a horizontal scrollbar. */
	#log>pre,
	.history-chat>pre {
		align-self: flex-start;
		max-width: min(620px, 82%);
		background: var(--surface);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) 4px;
		padding: 12px 14px;
		margin: 0;
		font-family: var(--font-body);
		line-height: 1.55;
		white-space: pre-wrap;
		word-wrap: break-word;
	}

	/* Dashed + monospace: stdout from LLM-written python run in the browser */
	#log>pre.local-output,
	.history-chat>pre.local-output {
		background: var(--surface-2);
		border-style: dashed;
		font-family: var(--font-mono);
		font-size: 14px;
	}

	#log>pre.local-output .tooltip-icon {
		margin-right: 6px;
	}

	/* !important overrides highlight.js's own padding on <code> */
	#log pre code,
	.history-chat pre code {
		font-family: var(--font-mono);
		font-size: 14px;
		border-radius: var(--radius-sm);
		display: block;
		margin-top: 6px;
		padding: 10px !important;
	}

	/* A parsed tool call — same bubble shell as #log>pre, but a little form
	instead of raw JSON: a name chip on the first line, then one two-part
	box per parameter. See the tool-call-template in index.html. */
	#log>.tool-call,
	.history-chat>.tool-call {
		align-self: flex-start;
		max-width: min(620px, 82%);
		background: var(--surface);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) 4px;
		padding: 12px 14px;
		display: flex;
		flex-direction: column;
		gap: 8px;
	}

	.tool-call-name-row {
		display: flex;
		align-items: baseline;
		gap: 8px;
		flex-wrap: wrap;
	}

	.tool-call-label {
		color: var(--text-muted);
	}

	.tool-call-params {
		display: flex;
		flex-direction: column;
		gap: 6px;
	}

	/* The name chip (one box) and each param row (one box split in two) share
	this shell — a box, drawn as a row of one or two cells. */
	.tool-call-box {
		display: flex;
		background: var(--surface-2);
		border: 1px solid var(--border);
		border-radius: var(--radius-sm);
		font-family: var(--font-mono);
		font-size: 14px;
		overflow: hidden;
	}

	.tool-call-name {
		padding: 3px 8px;
	}

	.tool-call-param-name,
	.tool-call-param-value {
		padding: 5px 9px;
		overflow-wrap: anywhere;
	}

	.tool-call-param-name {
		color: var(--text-muted);
		border-right: 1px solid var(--border);
		flex: 0 0 auto;
		max-width: 45%;
	}

	.tool-call-param-value {
		flex: 1;
	}

	.tool-call-output-value {
		padding: 3px 8px;
		white-space: pre-wrap;
	}

	.tool-call-output-input {
		width: 100%;
		background: var(--surface-2);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-sm);
		padding: 8px 10px;
		font-family: var(--font-mono);
		font-size: 14px;
		resize: vertical;
	}

	.tool-call-output-input:focus {
		border-color: var(--accent);
	}

	.tool-call-output-submit {
		align-self: flex-end;
		background: linear-gradient(135deg, var(--accent-strong), var(--accent));
		color: #fff;
		border: 1px solid transparent;
		border-radius: var(--radius-sm);
		padding: 7px 16px;
		font-weight: 600;
		transition: filter .15s ease;
	}

	.tool-call-output-submit:hover {
		filter: brightness(1.08);
	}

	.tool-call-output-input:disabled,
	.tool-call-output-submit:disabled {
		opacity: .5;
		cursor: default;
	}

	/* Token counts / latency, centred between turns */
	.usage-div {
		align-self: center;
		font-family: var(--font-mono);
		font-size: 14px;
		color: var(--text-faint);
		padding: 4px 10px;
	}

	.error-div,
	.warning-div {
		align-self: flex-start;
		max-width: min(620px, 82%);
		border-radius: var(--radius-md);
		padding: 10px 12px;
	}

	.error-div {
		background: var(--danger-wash);
		border: 1px solid var(--danger);
		color: var(--danger);
	}

	.warning-div {
		background: var(--warn-wash);
		border: 1px solid var(--warn);
		color: var(--warn);
	}

	#sampleAudio {
		width: 100%;
	}

	/* ---------- TEXT CHAT COMPOSER ----------
	Only mounted in llm mode, and only while the socket is open — main.js
	unhides it in ws.onopen and hides it again in ws.onclose. Disabled between
	sending and the end of the LLM's turn (setChatInputEnabled). */
	#textChatInput {
		flex-shrink: 0;
		display: flex;
		gap: 8px;
		padding: 14px 28px 20px;
		border-top: 1px solid var(--border);
	}

	/* display:flex above beats the UA's [hidden]{display:none} */
	#textChatInput[hidden] {
		display: none;
	}

	#chatMessageInput {
		flex: 1;
		min-width: 0;
		background: var(--surface-2);
		border: 1px solid var(--border);
		color: var(--text);
		border-radius: var(--radius-sm);
		padding: 9px 11px;
		font-family: var(--font-mono);
	}

	#chatMessageInput:focus {
		border-color: var(--accent);
	}

	#chatSendBtn {
		background: linear-gradient(135deg, var(--accent-strong), var(--accent));
		color: #fff;
		border: 1px solid transparent;
		border-radius: var(--radius-sm);
		padding: 9px 18px;
		font-weight: 600;
		transition: filter .15s ease;
	}

	#chatSendBtn:hover:not(:disabled) {
		filter: brightness(1.08);
	}

	#chatMessageInput:disabled,
	#chatSendBtn:disabled {
		opacity: .5;
		cursor: default;
	}

	/* ---------- MIC FAB ----------
	Mutes/unmutes the mic (or pauses the audio file) mid-session — it does not
	close the connection. .recording is owned by setMicActive() in main.js;
	amber + pulse means audio is actually flowing. */
	#stopRecording {
		position: absolute;
		right: 28px;
		bottom: 26px;
		width: 60px;
		height: 60px;
		border-radius: 50%;
		border: none;
		background: linear-gradient(135deg, var(--accent-strong), var(--accent));
		box-shadow: 0 4px 20px var(--accent-glow);
		display: flex;
		align-items: center;
		justify-content: center;
		padding: 0;
		color: #fff;
		z-index: 8;
		transition: box-shadow .2s ease, filter .2s ease, transform .1s ease;
	}

	/* display:flex above beats the UA's [hidden]{display:none}, so without this the
	FAB is visible on load and never hides for file input. */
	#stopRecording[hidden] {
		display: none;
	}

	#stopRecording:hover {
		transform: scale(1.04);
		filter: brightness(1.08);
		box-shadow: 0 6px 26px var(--accent-glow);
	}

	#stopRecording:active {
		transform: scale(0.97);
	}

	#stopRecording .mic-icon {
		width: 22px;
		height: 22px;
	}

	#stopRecording.recording {
		background: linear-gradient(135deg, #E8423D, #f0b95c);
		color: #241705;
		box-shadow: 0 4px 20px var(--warn-glow);
	}

	/* Expanding ring — decoration only, sits outside the button's box */
	#stopRecording.recording::before {
		content: "";
		position: absolute;
		inset: -3px;
		border-radius: 50%;
		border: 2px solid #e8423d;
		animation: pulse-ring 1.6s ease-out infinite;
	}

	#stopRecording::after {
		content: "OFF";
	}

	#stopRecording.recording::after {
		content: "ON";
	}

	@keyframes pulse-ring {
		0% {
			transform: scale(1);
			opacity: .6;
		}

		100% {
			transform: scale(1.55);
			opacity: 0;
		}
	}

	@media (prefers-reduced-motion:reduce) {
		#stopRecording.recording::before {
			animation: none;
		}

		#connectThrobber {
			animation: none;
		}

		.pulse,
		#app:not(.chat-live):has(#history-chat.visible) #newChatBtn {
			animation: none;
		}

		.history-item.deleting {
			animation-duration: .01s;
		}

		* {
			transition-duration: .01s !important;
		}
	}

	/* ============================================================
	MOBILE NAV TOGGLE — hidden above 900px
	============================================================ */
	#menuToggle {
		display: none;
		position: fixed;
		top: 14px;
		left: 14px;
		z-index: 30;
		width: 38px;
		height: 38px;
		background: var(--surface);
		border: 1px solid var(--border);
		border-radius: var(--radius-sm);
		flex-direction: column;
		align-items: center;
		justify-content: center;
		gap: 4px;
	}

	#menuToggle span {
		width: 16px;
		height: 2px;
		background: var(--text);
		border-radius: 2px;
	}

	/* ============================================================
	RESPONSIVE
	Below 900px the sidebars leave the grid and become off-canvas
	drawers driven by body.nav-open.
	============================================================ */
	@media (max-width:900px) {

		/* The zero-width tracks still have to exist — #main-content is pinned to
		grid-column:3 and would have nowhere to land otherwise. */
		#app {
			grid-template-columns: 0 0 1fr;
		}

		#rail,
		#config-pane {
			position: fixed;
			top: 0;
			bottom: 0;
			z-index: 25;
			transform: translateX(-100%);
			transition: transform .2s ease;
		}

		#rail {
			left: 0;
			width: var(--rail-w);
		}

		#config-pane {
			left: var(--rail-w);
			width: var(--config-w);
		}

		body.nav-open #rail,
		body.nav-open #config-pane {
			transform: translateX(0);
		}

		body.nav-open #sidebar-overlay {
			display: block;
		}

		#menuToggle {
			display: flex;
		}

		#main-content {
			padding-top: 54px;
		}

		/* clear the fixed hamburger */
		#stopRecording {
			right: 18px;
			bottom: 18px;
		}

		#textChatInput {
			padding: 12px 18px 16px;
		}
	}

	/* ============================================================
	LOGIN MODAL
	============================================================ */
	#login-modal {
		background: var(--surface);
		color: var(--text);
		border: 1px solid var(--border);
		border-radius: var(--radius-lg);
		padding: 24px;
		max-width: 360px;
	}

	#login-modal::backdrop {
		background: rgba(0, 0, 0, 0.55);
	}

	#login-modal p {
		font-family: var(--font-mono);
		color: var(--text-muted);
		margin: 0 0 14px;
	}

	#login-modal button {
		background: var(--accent-wash);
		color: var(--accent);
		border: 1px solid transparent;
		border-radius: var(--radius-sm);
		padding: 8px 14px;
		font-weight: 600;
	}

	/* ============================================================
	DATA NOTICE
	Shown once after login, before any audio or message can be sent.
	Uses the same tokens as everything else so it follows the theme —
	--bg-elevated / --text-primary don't exist in this file, and hardcoded
	fallbacks would leave it dark in light mode.
	============================================================ */
	.data-notice-overlay {
		position: fixed;
		inset: 0;
		display: flex;
		align-items: center;
		justify-content: center;
		background: rgba(0, 0, 0, 0.6);
		z-index: 1000;
		padding: 1rem;
	}

	.data-notice-box {
		background: var(--surface);
		color: var(--text);
		border: 1px solid var(--border);
		border-radius: var(--radius-lg);
		padding: 24px;
		max-width: 32rem;
		line-height: 1.5;
	}

	.data-notice-box p {
		margin: 0;
	}

	/* The global control reset only carries font and colour, so without this the
	Continue button renders as a raw UA button on the panel. */
	.data-notice-continue {
		margin-top: 1.25rem;
		background: var(--accent-wash);
		color: var(--accent);
		border: 1px solid transparent;
		border-radius: var(--radius-sm);
		padding: 8px 14px;
		font-weight: 600;
	}

	.data-notice-continue:hover {
		filter: brightness(1.15);
	}