/*
 * Huugl — application styles.
 *
 * One stylesheet, no framework: the whole UI is ~14 kB, which keeps the first
 * paint fast on mobile connections. Layout is CSS grid/flex with light/dark
 * aware custom properties. The palette follows MS Teams: an accent rail, a
 * quiet canvas, colour only where it means something.
 *
 * Themes: the JS resolves `settings.theme` (system | light | dark) onto
 * `<html data-theme>`. Before JS runs the OS preference decides via the media
 * query, so there is no flash of the wrong theme.
 */

/*
 * ------------------------------------------------------------- token layer
 *
 * One system for the whole UI (docs/UI_SPEC.md §2). Everything below is a
 * *token*: no component rule may name a colour, a radius, a shadow or an
 * easing curve of its own. The scales come first, then the two themes.
 *
 * The legacy names (`--bg`, `--border`, `--text-dim`, `--radius`, `--shadow`,
 * `--hover`, …) are kept as aliases of the spec names so the rules further
 * down this file — several of which belong to the panels, not to the shell —
 * keep working untouched.
 */

:root {
	/* --- spacing: 4 px base, only the steps the spec allows ------------- */
	--sp-2: 2px;
	--sp-4: 4px;
	--sp-6: 6px;
	--sp-8: 8px;
	--sp-12: 12px;
	--sp-16: 16px;
	--sp-20: 20px;
	--sp-24: 24px;
	--sp-32: 32px;
	--sp-40: 40px;
	--sp-48: 48px;

	/* --- radii --------------------------------------------------------- */
	--radius-sm: 4px; /* chips, inputs */
	--radius-md: 6px; /* rows, buttons */
	--radius-lg: 10px; /* cards, tiles */
	--radius-xl: 16px; /* dialogs */
	--radius-full: 999px; /* avatars, dots */

	/* --- type ---------------------------------------------------------- */
	--fs-11: 11px;
	--fs-12: 12px;
	--fs-13: 13px;
	--fs-14: 14px;
	--fs-16: 16px;
	--fs-20: 20px;
	--fs-24: 24px;
	--fs-28: 28px;
	--lh-prose: 1.4;
	--lh-heading: 1.25;
	/* Type ≥ 18 px tightens up, or the words drift apart. */
	--ls-display: -0.01em;

	/* --- motion: Fluent's decelerate curve ------------------------------ */
	--dur-fast: 120ms;
	--dur: 180ms;
	--dur-slow: 260ms;
	--ease: cubic-bezier(0.2, 0, 0, 1);

	/* --- density (spec §3/§4) ------------------------------------------
	 * Compact on desktop, comfortable on touch. Every row height, list gap
	 * and control size below reads one of these, so switching the mode is a
	 * single attribute write and no component needs its own media query. */
	--row-h: 44px;
	--row-pad: var(--sp-8);
	--list-gap: var(--sp-2);
	--control-h: 32px;
	--icon-size: 20px;

	/* --- chrome metrics ------------------------------------------------- */
	--topbar-h: 48px;
	--rail-w: 64px; /* spec §3: the activity rail is 64 px wide */
	--tile-gap: var(--sp-8);
	--scrim: rgba(0, 0, 0, 0.45); /* dialog backdrop */
	/* Video letterbox: the darkest surface we own, so a stream never washes
	   out against a light chrome. */
	--stage-bg: #000000;
	--on-stage: #ffffff; /* text over a gradient scrim on a tile */

	/* ============================================ theme: "day" (default) */
	/* Neutral warm greys (Fluent neutral base), one purposeful brand purple. */
	--bg-canvas: #f3f2f1;
	--bg-surface: #ffffff;
	--bg-elevated: #ffffff;
	--bg-overlay: #ffffff;
	--bg-hover: #f3f2f1;
	--bg-active: #edebe9;
	--bg-selected: rgba(91, 95, 199, 0.08);
	--bg-accent-soft: rgba(91, 95, 199, 0.1);

	--text-primary: #242424;
	--text-secondary: #616161;
	--text-tertiary: #8a8886;
	--text-on-accent: #ffffff;

	--border-subtle: #edebe9;
	--border-default: #e1dfdd;
	--border-strong: #c8c6c4;

	--accent: #5b5fc7;
	--accent-hover: #4f52b2;
	--accent-active: #464a9e;
	--accent-soft: rgba(91, 95, 199, 0.12);
	--accent-contrast: #ffffff;

	/* Status hues. Never decoration — only ever a state a user has to read. */
	--positive: #0e700e;
	--caution: #986f0b;
	--critical: #c43131;
	--info: #616161;
	--positive-soft: rgba(14, 112, 14, 0.1);
	--caution-soft: rgba(152, 111, 11, 0.12);
	--critical-soft: rgba(196, 49, 49, 0.1);

	--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.06), 0 0 2px rgba(0, 0, 0, 0.04);
	--shadow-2: 0 4px 12px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06);
	--shadow-3: 0 12px 32px rgba(0, 0, 0, 0.14), 0 2px 8px rgba(0, 0, 0, 0.08);

	/* Mica: the rail and the top bar float over the canvas on capable
	 * browsers. `.mica` rules below fall back to the opaque token when
	 * backdrop-filter is missing, so this never leaves text unreadable. */
	--mica-blur: blur(20px) saturate(1.4);
	--rail-tint: rgba(255, 255, 255, 0.72);

	color-scheme: light;

	/* --- aliases for the names the rest of the stylesheet already uses --- */
	--bg: var(--bg-canvas);
	--bg-panel: var(--bg-surface);
	--hover: var(--bg-hover);
	--rail-bg: var(--bg-surface);
	--rail-fg: var(--text-secondary);
	--border: var(--border-default);
	--text: var(--text-primary);
	--text-dim: var(--text-secondary);
	--accent-strong: var(--accent-hover);
	--danger: var(--critical);
	--danger-soft: var(--critical-soft);
	--success: var(--positive);
	--warning: var(--caution);
	--radius: var(--radius-lg);
	--shadow: var(--shadow-3);
}

/* Teams "night": a real second theme, not an inversion. The dark neutral ramp
 * is its own set of steps (never the light ones lightened), the accent is
 * tuned for AA on a dark canvas, and the shadows lose their opacity because a
 * black shadow on a near-black surface is invisible. */
:root[data-theme="dark"] {
	--bg-canvas: #1b1a19;
	--bg-surface: #292828;
	--bg-elevated: #323130;
	--bg-overlay: #323130;
	--bg-hover: rgba(255, 255, 255, 0.06);
	--bg-active: rgba(255, 255, 255, 0.09);
	--bg-selected: rgba(146, 148, 255, 0.16);
	--bg-accent-soft: rgba(146, 148, 255, 0.2);

	--text-primary: #f3f2f1;
	--text-secondary: #d2d0ce;
	--text-tertiary: #a19f9d;
	--text-on-accent: #0b0a0a;

	--border-subtle: #3b3a39;
	--border-default: #484644;
	--border-strong: #605e5c;

	--accent: #9294ff;
	--accent-hover: #a6a7ff;
	--accent-active: #7b7de8;
	--accent-soft: rgba(146, 148, 255, 0.18);
	--accent-contrast: #0b0a0a;

	--positive: #6ccb5f;
	--caution: #fce100;
	--critical: #ff99a4;
	--info: #d2d0ce;
	--positive-soft: rgba(108, 203, 95, 0.16);
	--caution-soft: rgba(252, 225, 0, 0.14);
	--critical-soft: rgba(255, 153, 164, 0.16);

	--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
	--shadow-2: 0 4px 12px rgba(0, 0, 0, 0.45);
	--shadow-3: 0 12px 32px rgba(0, 0, 0, 0.55);

	--rail-tint: rgba(43, 42, 41, 0.72);

	color-scheme: dark;
}

/* OS preference until (and unless) JS has resolved `settings.theme`. */
@media (prefers-color-scheme: dark) {
	:root:not([data-theme]) {
		--bg-canvas: #1b1a19;
		--bg-surface: #292828;
		--bg-elevated: #323130;
		--bg-overlay: #323130;
		--bg-hover: rgba(255, 255, 255, 0.06);
		--bg-active: rgba(255, 255, 255, 0.09);
		--bg-selected: rgba(146, 148, 255, 0.16);
		--bg-accent-soft: rgba(146, 148, 255, 0.2);

		--text-primary: #f3f2f1;
		--text-secondary: #d2d0ce;
		--text-tertiary: #a19f9d;
		--text-on-accent: #0b0a0a;

		--border-subtle: #3b3a39;
		--border-default: #484644;
		--border-strong: #605e5c;

		--accent: #9294ff;
		--accent-hover: #a6a7ff;
		--accent-active: #7b7de8;
		--accent-soft: rgba(146, 148, 255, 0.18);
		--accent-contrast: #0b0a0a;

		--positive: #6ccb5f;
		--caution: #fce100;
		--critical: #ff99a4;
		--info: #d2d0ce;
		--positive-soft: rgba(108, 203, 95, 0.16);
		--caution-soft: rgba(252, 225, 0, 0.14);
		--critical-soft: rgba(255, 153, 164, 0.16);

		--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
		--shadow-2: 0 4px 12px rgba(0, 0, 0, 0.45);
		--shadow-3: 0 12px 32px rgba(0, 0, 0, 0.55);

		--rail-tint: rgba(43, 42, 41, 0.72);

		color-scheme: dark;
	}
}

/* ---------------------------------------------------------- density modes
 * One attribute on <html>, everything downstream through the tokens above
 * (spec §3): no component carries a "comfortable" variant of its own. */

:root[data-density="comfortable"] {
	--row-h: 52px;
	--row-pad: var(--sp-12);
	--list-gap: var(--sp-4);
	--control-h: 36px;
}

/* Touch wins over the preference: comfortable is the only sane spacing for a
 * finger, whatever the user picked for their mouse. */
@media (pointer: coarse) {
	:root {
		--row-h: 52px;
		--row-pad: var(--sp-12);
		--list-gap: var(--sp-4);
		--control-h: 44px;
	}
}

/* Mica helper. `backdrop-filter` is decorative, so the opaque token has to be
 * the fallback — otherwise an unsupported browser gets a see-through rail. */
.mica {
	background: var(--bg-surface);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
	.mica {
		background: var(--rail-tint);
		-webkit-backdrop-filter: var(--mica-blur);
		backdrop-filter: var(--mica-blur);
	}
}

* {
	box-sizing: border-box;
}

/* Several components set an explicit `display`, which would otherwise win over
   the user-agent rule for the `hidden` attribute and keep hidden views (lobby,
 * call, side panel) in the layout. */
[hidden] {
	display: none !important;
}

html,
body {
	margin: 0;
	height: 100%;
	background: var(--bg-canvas);
	color: var(--text-primary);
	font: var(--fs-14)/var(--lh-prose) "Segoe UI", system-ui, -apple-system, Roboto, "Helvetica Neue", sans-serif;
	-webkit-font-smoothing: antialiased;
	/* Comfortable reading on touch (spec §2: body 15–16 px there). */
	overscroll-behavior: none;
}

@media (pointer: coarse) {
	html,
	body {
		font-size: 15px;
	}
}

/* One focus ring for the whole app (spec §7): accent, 2 px, offset 2 px.
   `:focus-visible` so a mouse click never leaves a halo behind. */
:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
	border-radius: var(--radius-sm);
}

/* Visible to a screen reader, invisible on screen: labels of icon-only
   controls, live status text, and the short keys in the search field. */
.sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* ---------------------------------------------------------------- buttons */

.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--sp-6);
	min-height: var(--control-h);
	padding: var(--sp-6) var(--sp-12);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-md);
	background: var(--bg-elevated);
	color: var(--text-primary);
	font: inherit;
	cursor: pointer;
	/* Only paint-level properties: a transition on `border-color` used to
	   relayout the surrounding row on every pointer pass. */
	transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.btn:hover {
	background: var(--bg-hover);
	border-color: var(--border-strong);
}

.btn:active {
	transform: translateY(1px);
}

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

.btn:disabled {
	opacity: 0.55;
	cursor: default;
}

.btn--primary {
	background: var(--accent);
	border-color: var(--accent);
	color: var(--text-on-accent);
}

.btn--primary:hover {
	background: var(--accent-hover);
	border-color: var(--accent-hover);
}

.btn--primary:active {
	background: var(--accent-active);
	border-color: var(--accent-active);
}

.btn--danger {
	background: var(--critical);
	border-color: var(--critical);
	color: var(--text-on-accent);
}

.btn--danger:hover {
	filter: brightness(1.06);
}

.btn--ghost {
	background: transparent;
	border-color: transparent;
	color: var(--text-secondary);
}

.btn--ghost:hover {
	background: var(--bg-hover);
	border-color: transparent;
	color: var(--text-primary);
}

.btn--sm {
	padding: var(--sp-4) var(--sp-8);
	font-size: var(--fs-13);
}

.btn--block {
	width: 100%;
}

.btn--round {
	position: relative;
	width: 46px;
	height: 46px;
	padding: 0;
	border-radius: var(--radius-full);
	font-size: var(--fs-20);
	background: var(--bg-surface);
}

.btn--round.is-off {
	background: var(--danger);
	border-color: var(--danger);
}

.btn--round.is-active {
	background: var(--accent);
	border-color: var(--accent);
}

/* Microphone level indicator drawn with a conic gradient.
 *
 * The ring sits *on top of* the button (it used to be painted behind it with
 * `z-index: -1`, which left only a sliver of the 3 px band visible) and is as
 * thick as the offset, so the whole band shows. `--level` is 0..1 and is only
 * written while the microphone is actually live. */
#btnMic::after,
#lobbyToggleMic::after {
	content: "";
	position: absolute;
	inset: -4px;
	padding: 4px;
	border-radius: 50%;
	background: conic-gradient(var(--success) calc(var(--level, 0) * 360deg), transparent 0);
	-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 4px));
	mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 4px));
	filter: drop-shadow(0 0 4px rgba(48, 164, 108, 0.55));
	pointer-events: none;
}

/* A hint of the ring at rest, so the meter is findable without speaking. */
#btnMic:not(.is-off)::before,
#lobbyToggleMic:not(.is-off)::before {
	content: "";
	position: absolute;
	inset: -4px;
	border-radius: 50%;
	border: 1px solid rgba(48, 164, 108, 0.45);
	pointer-events: none;
}

/* Muted: no meter, and a clearly red button (from `.btn--round.is-off`). */
#btnMic.is-off::after,
#lobbyToggleMic.is-off::after {
	display: none;
}

/* ------------------------------------------------------------------ lobby */

/* The lobby lives inside the shell's content pane; it centres the pre-join
   card like Teams' "Join now" page. */
.lobby {
	display: grid;
	place-items: center;
	min-height: 100%;
	padding: var(--sp-24) var(--sp-16) var(--sp-40);
}

.lobby__card {
	width: min(760px, 100%);
	padding: var(--sp-24);
	background: var(--bg-elevated);
	border: 1px solid var(--border-subtle);
	border-radius: var(--radius-xl);
	box-shadow: var(--shadow-2);
}

/* Wide screens: Teams pre-join look — big camera preview left, form right. */
@media (min-width: 720px) {
	.lobby__card {
		display: grid;
		grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
		gap: 6px 24px;
		align-items: start;
	}

	.lobby__header {
		grid-column: 1 / -1;
		text-align: left;
	}

	.lobby__preview {
		margin-bottom: 0;
	}
}

.lobby__header {
	text-align: center;
	margin-bottom: var(--sp-16);
}

.lobby__heading {
	margin: 0;
	font-size: var(--fs-24);
	line-height: var(--lh-heading);
	letter-spacing: var(--ls-display);
	font-weight: 600;
	color: var(--text-primary);
}

.lobby__tagline {
	margin: var(--sp-4) 0 0;
	color: var(--text-secondary);
	font-size: var(--fs-14);
}

/* The video box is the one place in the shell that is always dark: a camera
   preview on a light canvas reads as a broken image. */
.lobby__preview {
	position: relative;
	aspect-ratio: 4 / 3;
	margin-bottom: var(--sp-16);
	background: var(--stage-bg);
	border-radius: var(--radius-lg);
	overflow: hidden;
}

/* Never mirrored: the preview shows the camera exactly as it is captured. */
.lobby__video {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.lobby__previewControls {
	position: absolute;
	inset-inline: 0;
	bottom: var(--sp-12);
	display: flex;
	justify-content: center;
	align-items: center;
	gap: var(--sp-12);
	padding: 0 var(--sp-12);
}

.lobby__error {
	margin: 0 0 var(--sp-12);
	padding: var(--sp-12) var(--sp-16);
	border-radius: var(--radius-md);
	background: var(--critical-soft);
	border: 1px solid var(--critical);
	color: var(--critical);
	font-size: var(--fs-14);
}

/* Invite link under the pre-join form: the room travels in the URL fragment,
   so what is shown here is literally what a recipient pastes into the
   address bar. */
.lobby__share {
	margin: 0 0 var(--sp-12);
	padding-top: var(--sp-12);
	border-top: 1px solid var(--border-subtle);
}

.lobby__shareRow {
	display: flex;
	gap: var(--sp-8);
}

.lobby__shareInput {
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: var(--fs-13);
	overflow: hidden;
	text-overflow: ellipsis;
}

.lobby__shareButton {
	flex: 0 0 auto;
}

.lobby__share code {
	padding: 1px var(--sp-4);
	border-radius: var(--radius-sm);
	background: var(--bg-hover);
	font-size: var(--fs-12);
}

.lobby__devices {
	margin-top: var(--sp-16);
	border-top: 1px solid var(--border-subtle);
	padding-top: var(--sp-12);
}

.lobby__devices summary {
	display: flex;
	align-items: center;
	/* ≥ 44 px on touch: the summary is the only way into the device list. */
	min-height: var(--control-h);
	cursor: pointer;
	color: var(--text-secondary);
	font-size: var(--fs-14);
}

.lobby__devices summary::marker {
	color: var(--accent);
}

.lobby__devices[open] summary {
	margin-bottom: var(--sp-12);
	color: var(--text-primary);
	font-weight: 600;
}

.lobby__devicesBody {
	animation: fade-in var(--dur) var(--ease);
}

.field {
	display: block;
	margin-bottom: var(--sp-12);
}

.field__label {
	display: block;
	margin-bottom: var(--sp-4);
	font-size: var(--fs-13);
	color: var(--text-secondary);
}

.field__input {
	width: 100%;
	min-height: var(--control-h);
	padding: var(--sp-8) var(--sp-12);
	border: 1px solid var(--border-default);
	border-bottom-color: var(--border-strong);
	border-radius: var(--radius-sm);
	background: var(--bg-canvas);
	color: var(--text-primary);
	font: inherit;
	transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.field__input::placeholder {
	color: var(--text-tertiary);
}

.field__input:hover {
	background: var(--bg-hover);
}

/* A focused field is underlined in the accent, like Fluent's inputs: the ring
   alone disappears behind the keyboard on touch. */
.field__input:focus {
	outline: none;
	border-color: var(--accent);
	border-bottom-width: 2px;
	background: var(--bg-surface);
}

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

.field__input:disabled {
	opacity: 0.55;
}

/* Selects need their own chevron room, otherwise the label overlaps it. */
select.field__input {
	appearance: none;
	padding-right: var(--sp-24);
	background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
	background-position: right 14px center, right 9px center;
	background-size: 5px 5px, 5px 5px;
	background-repeat: no-repeat;
}

input[type="range"].field__input {
	padding: 0;
	height: var(--control-h);
	background: transparent;
	border: 0;
	accent-color: var(--accent);
}

/* The live percentage next to a volume slider. */
.field__value {
	float: right;
	font-variant-numeric: tabular-nums;
	color: var(--accent);
}

.checkbox {
	display: flex;
	align-items: center;
	gap: var(--sp-8);
	min-height: var(--control-h);
	margin-bottom: var(--sp-2);
	font-size: var(--fs-14);
	color: var(--text-primary);
	cursor: pointer;
}

.checkbox input {
	accent-color: var(--accent);
	width: 16px;
	height: 16px;
	flex: none;
	cursor: pointer;
}

/* ------------------------------------------------------------------- call */

.call {
	display: grid;
	grid-template-rows: auto 1fr auto;
	height: 100%;
	position: relative;
}

.call__header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sp-12);
	padding: var(--sp-8) var(--sp-16);
	padding-top: calc(var(--sp-8) + env(safe-area-inset-top));
	background: var(--bg-surface);
	border-bottom: 1px solid var(--border-subtle);
	/* `min-height: 0` lets the row collapse completely in immersive mode. */
	min-height: 0;
	overflow: hidden;
	flex: none;
}

.call__meta {
	display: flex;
	align-items: baseline;
	gap: var(--sp-12);
	min-width: 0;
}

.call__room {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-size: var(--fs-14);
	font-weight: 600;
	color: var(--text-primary);
}

.call__timer {
	color: var(--text-secondary);
	font-size: var(--fs-12);
	font-variant-numeric: tabular-nums;
}

.call__status {
	display: flex;
	align-items: center;
	gap: var(--sp-8);
	font-size: var(--fs-12);
}

.call__stats {
	color: var(--text-dim);
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.call__stage {
	position: relative;
	overflow: hidden;
	/* Without this the grid grows with the intrinsic video height and pushes
	   the control bar below the fold on short screens. */
	min-height: 0;
}

/* -------------------------------------------------------- immersive chrome */

/* With the controls hidden the grid rows collapse, so the videos get the whole
   window. Any pointer movement or key press removes `is-idle` again. */
.call.is-idle {
	grid-template-rows: 0 minmax(0, 1fr) 0;
}

.call.is-idle .call__header,
.call.is-idle .controls {
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
	height: 0;
	padding-block: 0;
	border-width: 0;
}

.call__header,
.controls {
	/* Opacity only: the chrome fades in place, it never slides over the video. */
	transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.call.is-idle .grid {
	padding: 0;
	gap: 1px;
}

/* A quiet reminder that the controls are one mouse move away. */
.call.is-idle .call__stage::after {
	content: "";
	position: absolute;
	inset-inline: 0;
	top: 0;
	height: 6px;
	background: linear-gradient(to bottom, rgba(0, 0, 0, 0.35), transparent);
	pointer-events: none;
}

/* ------------------------------------------------------------------- grid */

.grid {
	display: grid;
	grid-template-columns: repeat(var(--cols, 1), minmax(0, 1fr));
	grid-auto-rows: minmax(0, 1fr);
	gap: var(--tile-gap);
	height: 100%;
	min-height: 0;
	padding: var(--tile-gap);
}

/* Plain grid: everybody equal. */
.grid[data-layout="grid"] {
	grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
}

/* Speaker: one big tile, the rest as a filmstrip underneath. */
.grid[data-layout="speaker"] {
	grid-template-columns: repeat(var(--strip-cols, 3), minmax(0, 1fr));
	grid-template-rows: minmax(0, 1fr) auto;
	grid-auto-rows: clamp(70px, 16vh, 132px);
}

.grid[data-layout="speaker"] .tile--main,
.grid[data-layout="speaker"] .tile--pinned {
	grid-column: 1 / -1;
	grid-row: 1;
}

.grid[data-layout="speaker"] .tile:not(.tile--main):not(.tile--pinned) {
	grid-row: 2;
	height: clamp(70px, 16vh, 132px);
}

/* Sidebar: one big tile, the rest in a column on the right. */
.grid[data-layout="sidebar"] {
	grid-template-columns: minmax(0, 1fr) clamp(140px, 22%, 280px);
	grid-template-rows: minmax(0, 1fr);
	grid-auto-rows: minmax(0, 1fr);
}

.grid[data-layout="sidebar"] .tile--main,
.grid[data-layout="sidebar"] .tile--pinned {
	grid-column: 1;
	grid-row: 1;
}

.grid[data-layout="sidebar"] .tile:not(.tile--main):not(.tile--pinned) {
	grid-column: 2;
}

/* Focus: a single tile; the others stay in the DOM (and keep playing) but are
   not shown, so switching is instant. */
.grid[data-layout="focus"] .tile--hidden {
	display: none;
}

/* A pinned tile always takes the whole stage, whatever the layout is. */
.grid[data-layout="grid"] .tile--pinned {
	grid-column: 1 / -1;
	grid-row: 1 / -1;
}

.grid[data-layout="grid"] .tile--pinned~.tile {
	display: none;
}

.tile {
	position: relative;
	min-height: 0;
	min-width: 0;
	background: var(--stage-bg);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	overflow: hidden;
}

/*
 * Video is never mirrored — not the local camera, not the local screen share
 * (a flipped screen share shows unreadable text) and not the remote tiles.
 * The local tile shows the screen share as soon as it starts, so a mirror here
 * would break both.
 *
 * `object-fit` is a variable, not a value: "fit" (letterbox over
 * `--stage-bg`) is the default and the only mode that loses nothing, and the
 * explicit "fill" presentation mode sets `--tile-fit: cover` on the grid.
 */
.tile__video {
	width: 100%;
	height: 100%;
	object-fit: var(--tile-fit, contain);
	background: var(--stage-bg);
}

.tile--pinned {
	grid-column: 1 / -1;
	grid-row: 1 / -1;
}

.tile--hidden {
	display: none;
}

.tile--audio-only .tile__video {
	visibility: hidden;
}

.tile--audio-only::before {
	content: attr(data-initial);
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	font-size: 42px;
	color: var(--text-dim);
}

.tile--connecting {
	opacity: 0.65;
}

/* Speaking indicator: a level bar along the bottom edge of the tile. It stays
   visible while the controls are auto-hidden; `--level` (0..1) is written by
   `VideoGrid.setLevel()`, and only when the level actually changes. */
.tile::after {
	content: "";
	position: absolute;
	left: 0;
	bottom: 0;
	width: calc(var(--level, 0) * 100%);
	height: 4px;
	background: var(--success);
	box-shadow: 0 0 6px rgba(48, 164, 108, 0.8);
	pointer-events: none;
	z-index: 2;
	transition: width 0.08s linear;
}

/* Somebody is talking: make the tile itself react, so the indicator is not
   only a 4 px line in a dark video. */
.tile--speaking {
	box-shadow: 0 0 0 2px var(--success);
}

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

/* Tile actions: only while the pointer is over the tile (or the tile has
   keyboard focus), so they never eat space during the call. */
.tile__actions {
	position: absolute;
	top: 8px;
	left: 8px;
	display: flex;
	gap: 4px;
	opacity: 0;
	transform: translateY(-4px);
	transition: opacity 0.15s ease, transform 0.15s ease;
	pointer-events: none;
}

.tile:hover .tile__actions,
.tile:focus-within .tile__actions {
	opacity: 1;
	transform: translateY(0);
	pointer-events: auto;
}

.tile__action {
	width: 30px;
	height: 30px;
	padding: 0;
	border: 1px solid rgba(255, 255, 255, 0.18);
	border-radius: 8px;
	background: rgba(12, 14, 20, 0.7);
	color: #fff;
	font-size: 14px;
	line-height: 1;
	cursor: pointer;
	backdrop-filter: blur(4px);
}

.tile__action:hover {
	background: var(--accent);
	border-color: var(--accent);
}

/* Per-tile statistics corner, filled by the stats overlay. */
.tile__stats {
	position: absolute;
	top: 8px;
	right: 8px;
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: 2px;
	padding: 4px 6px;
	border-radius: 6px;
	background: rgba(12, 14, 20, 0.62);
	color: #d7dce6;
	font: 11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;
	pointer-events: none;
}

.tile__overlay {
	position: absolute;
	inset-inline: 0;
	bottom: 0;
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 6px 8px;
	background: linear-gradient(to top, rgba(0, 0, 0, 0.65), transparent);
	font-size: 13px;
}

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

.tile__badges {
	display: flex;
	gap: 4px;
}

.tile__quality::before {
	content: "";
	display: inline-block;
	width: 8px;
	height: 8px;
	border-radius: 50%;
	background: var(--success);
}

.tile__quality[data-quality="ok"]::before {
	background: var(--warning);
}

.tile__quality[data-quality="poor"]::before {
	background: var(--danger);
}

.tile__reaction {
	position: absolute;
	top: 8px;
	right: 8px;
	font-size: 34px;
	opacity: 0;
	transform: scale(0.6);
	transition: transform 0.2s ease, opacity 0.2s ease;
	pointer-events: none;
}

.tile__reaction--visible {
	opacity: 1;
	transform: scale(1);
}

/* --------------------------------------------------------------- controls */

.controls {
	display: flex;
	align-items: center;
	justify-content: center;
	/* 13 round buttons do not fit a phone in portrait: they wrap to the next
	   line instead of overflowing, which is what used to push #btnLeave — the
	   only way out of the call — off the screen. */
	flex-wrap: wrap;
	gap: 8px;
	padding: 10px max(12px, env(safe-area-inset-right)) calc(10px + env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
	background: var(--bg-elevated);
	border-top: 1px solid var(--border);
	/* Positioning context for the layout menu, the help popover and the
	   reaction picker, which all anchor to the bar. */
	position: relative;
	min-height: 0;
	/* Must stay `visible`: the popovers and the reaction picker escape upwards
	   out of the bar. */
	overflow: visible;
}

/* Wrapping must never squash a button into an unreadable sliver. */
.controls > .btn--round {
	flex: 0 0 auto;
}

.controls__spacer {
	flex: 1;
}

.reaction-picker {
	position: absolute;
	bottom: 60px;
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 4px;
	max-width: calc(100vw - 16px);
	padding: 6px;
	background: var(--bg-panel);
	border: 1px solid var(--border);
	border-radius: 999px;
	box-shadow: var(--shadow);
}

.reaction-picker button {
	border: 0;
	background: transparent;
	font-size: 20px;
	cursor: pointer;
}

/* --------------------------------------------------------------- popovers */

/* The layout menu and the shortcuts pop over the control bar, so they never
   reserve space. A popover is a small overlay surface: overlay background,
   raised radius, one level of shadow, and it animates in from the edge it is
   anchored to. */
.popover {
	position: absolute;
	bottom: calc(100% - 4px);
	right: var(--sp-12);
	z-index: 30;
	min-width: 220px;
	padding: var(--sp-4);
	background: var(--bg-overlay);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-lg);
	box-shadow: var(--shadow-3);
	animation: fade-in var(--dur-fast) var(--ease);
}

.popover--up {
	bottom: calc(100% - 4px);
	/* Above the (possibly wrapped) control bar there is not much room on a
	   phone, and a popover that cannot be scrolled out of reach is as bad as a
	   missing one. */
	max-height: min(60vh, 420px);
	overflow-y: auto;
	overscroll-behavior: contain;
}

.popover--wide {
	min-width: 320px;
	max-width: 92vw;
}

#layoutMenu {
	right: 120px;
}

/* Section label inside a popover. */
.popover__title {
	margin: var(--sp-4) var(--sp-8) var(--sp-6);
	font-size: var(--fs-11);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--text-tertiary);
}

.popover__item {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--sp-12);
	width: 100%;
	min-height: var(--control-h);
	padding: var(--sp-6) var(--sp-8);
	border: 0;
	border-radius: var(--radius-md);
	background: transparent;
	color: var(--text-primary);
	font: inherit;
	text-align: left;
	cursor: pointer;
	transition: background var(--dur-fast) var(--ease);
}

.popover__item small {
	color: var(--text-tertiary);
	font-size: var(--fs-12);
}

.popover__item:hover {
	background: var(--bg-hover);
}

.popover__item.is-active {
	background: var(--bg-selected);
	color: var(--accent);
}

.popover__list {
	margin: 0 0 var(--sp-8);
	padding-left: var(--sp-20);
	color: var(--text-secondary);
	font-size: var(--fs-13);
}

.popover__list li {
	margin: var(--sp-4) 0;
}

.popover__list b {
	color: var(--text-primary);
	font-weight: 600;
}

@keyframes fade-in {
	from {
		opacity: 0;
		transform: translateY(4px);
	}

	to {
		opacity: 1;
		transform: translateY(0);
	}
}

.dot {
	position: absolute;
	top: 4px;
	right: 4px;
	width: 10px;
	height: 10px;
	border-radius: 50%;
	background: var(--accent);
	display: none;
}

.dot.is-visible {
	display: block;
}

/* ------------------------------------------------------------- side panel */

.sidePanel {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	width: min(360px, 100%);
	display: flex;
	flex-direction: column;
	background: var(--bg-panel);
	border-left: 1px solid var(--border);
	box-shadow: var(--shadow);
}

.sidePanel__header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 10px 12px;
	border-bottom: 1px solid var(--border);
}

.sidePanel__title {
	margin: 0;
	font-size: 15px;
}

.panel {
	display: flex;
	flex-direction: column;
	overflow: auto;
	flex: 1;
	min-height: 0;
}

/* Scroll container of the participants panel. `flex: 1` + `min-height: 0`
   make *this* the scroller (the panel around it is the flex column), so the
   sticky overview stays put while a long roster moves under it. */
.panel__body {
	padding: 0 10px 10px;
	overflow-y: auto;
	flex: 1;
	min-height: 0;
}

.panel__title {
	margin: 12px 0 8px;
	font-size: 13px;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--text-dim);
}

/* A participant is one line: avatar, name, status, actions. No separators —
   the row rhythm carries the grouping, and 40 of them fit the panel. */
.row {
	display: flex;
	align-items: center;
	gap: 8px;
	min-height: 34px;
	padding: 3px 6px;
	border-radius: 8px;
}

.row__avatar {
	display: grid;
	place-items: center;
	flex: none;
	width: 26px;
	height: 26px;
	border-radius: 50%;
	background: var(--accent-soft);
	color: var(--text);
	font-size: 11px;
	font-weight: 600;
}

.row__text {
	flex: 1;
	min-width: 0;
}

.row__title {
	display: block;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* Visible to a screen reader, invisible on screen: the full words behind every
   status icon, so a compact row never loses information. */
.sr-only,
.row__chipLabel {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

.row__status {
	display: flex;
	align-items: center;
	gap: 3px;
	flex: none;
}

.row__chip {
	display: inline-grid;
	place-items: center;
	min-width: 18px;
	height: 18px;
	padding: 0 2px;
	border-radius: 5px;
	font-size: 11px;
	font-style: normal;
	line-height: 1;
	color: var(--text-dim);
}

/* Tones are theme variables, never hard-coded colours: the muted/off state
   reads as a problem, the rest is quiet. */
.row__chip--on {
	color: var(--text-dim);
}

.row__chip--off {
	background: var(--danger-soft);
	color: var(--danger);
}

.row__chip--accent {
	color: var(--accent);
}

.row__chip--warn {
	color: var(--warning);
}

.row__actions {
	display: flex;
	align-items: center;
	gap: 2px;
	flex: none;
}

/* ------------------------------------------------------------------- chat */

.chat {
	display: grid;
	grid-template-rows: 1fr auto auto;
	min-height: 0;
	flex: 1;
}

.chat__list {
	overflow-y: auto;
	padding: 12px;
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.bubble {
	max-width: 85%;
	padding: 8px 10px;
	border-radius: 12px;
	background: var(--bg-elevated);
	word-break: break-word;
}

/* Sent messages sit on the accent — the Teams chat look. */
.bubble--mine {
	align-self: flex-end;
	background: var(--accent);
	color: #fff;
}

.bubble--mine .bubble__meta {
	color: rgba(255, 255, 255, 0.75);
}

.bubble__meta {
	display: flex;
	gap: 6px;
	font-size: 11px;
	color: var(--text-dim);
}

.bubble__body {
	margin-top: 2px;
	white-space: pre-wrap;
}

/* The delete affordance appears on hover of own messages. */
.bubble__delete {
	border: 0;
	background: transparent;
	color: inherit;
	font: inherit;
	font-size: 11px;
	line-height: 1;
	padding: 1px 2px;
	margin-left: auto;
	cursor: pointer;
	opacity: 0;
	transition: opacity 0.1s ease;
}

.bubble:hover .bubble__delete,
.bubble__delete:focus-visible {
	opacity: 0.75;
}

.bubble__delete:hover {
	opacity: 1;
	color: var(--danger);
}

.chat__typing {
	min-height: 18px;
	padding: 0 12px;
	font-size: 12px;
	color: var(--text-dim);
	font-style: italic;
}

.chat__form {
	display: flex;
	gap: 8px;
	padding: 10px;
	border-top: 1px solid var(--border);
}

.chat__input {
	flex: 1;
	padding: 9px 10px;
	border: 1px solid var(--border);
	border-radius: 10px;
	background: var(--bg);
	color: var(--text);
	font: inherit;
}

/* --------------------------------------------------------------- settings */

.settings {
	padding: 12px;
	overflow: auto;
	/* Flex children shrink below their content by default, which made the whole
	   panel collapse to zero height and look like a broken settings page. */
	flex: 1;
	min-height: 0;
}

/* --------------------------------------------------------- participants */

/* Sticky overview: headcount, breakdown and the filter stay visible while the
   roster scrolls, so a big room is summarised at all times. */
.roster__head {
	position: sticky;
	top: 0;
	z-index: 2;
	padding: 8px 0 6px;
	background: var(--bg-panel);
	border-bottom: 1px solid var(--border);
}

.roster__title {
	margin: 0 0 5px;
}

.roster__stats {
	display: flex;
	flex-wrap: wrap;
	gap: 4px;
}

.roster__stat {
	padding: 1px 7px;
	border: 1px solid var(--border);
	border-radius: 999px;
	background: var(--bg-elevated);
	color: var(--text-dim);
	font-size: 11.5px;
	white-space: nowrap;
}

.roster__stat b {
	color: var(--text);
	font-weight: 600;
	font-variant-numeric: tabular-nums;
}

.roster__filter {
	width: 100%;
	margin-top: 6px;
	padding: 4px 8px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--bg-elevated);
	color: var(--text);
	font: inherit;
	font-size: 12.5px;
}

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

.roster__footer {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 2px;
	padding-top: 6px;
}

.roster__count {
	margin: 0;
	color: var(--text-dim);
	font-size: 11.5px;
	font-variant-numeric: tabular-nums;
}

.roster__more {
	width: 100%;
}

.panel__hint,
.panel__empty {
	margin: 8px 0 4px;
	color: var(--text-dim);
	font-size: 12px;
	line-height: 1.4;
}

.row--clickable {
	cursor: pointer;
	transition: background 0.12s ease;
}

.row--clickable:hover {
	background: var(--bg-elevated);
}

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

.row--self {
	background: var(--accent-soft);
}

.row.is-pinned {
	box-shadow: inset 3px 0 0 var(--accent);
}

.row__you {
	margin-left: 5px;
	color: var(--text-dim);
	font-weight: 400;
}

.row__role {
	margin-left: 5px;
	padding: 0 6px;
	border-radius: 999px;
	background: var(--bg-elevated);
	color: var(--text-dim);
	font-size: 10.5px;
}

.row .btn--round.btn--sm {
	width: 28px;
	height: 28px;
	font-size: 13px;
}

/* ------------------------------------------------------ stats overlay */

.statsOverlay {
	position: absolute;
	left: 12px;
	top: 12px;
	z-index: 20;
	width: min(340px, calc(100% - 24px));
	max-height: min(60vh, 420px);
	overflow: auto;
	padding: 10px 12px;
	background: rgba(255, 255, 255, 0.86);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: var(--shadow);
	backdrop-filter: blur(6px);
	font-size: 12px;
	color: var(--text);
}

.statsOverlay__head {
	display: flex;
	align-items: center;
	justify-content: space-between;
}

.statsOverlay__title {
	margin: 0;
	font-size: 13px;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--text-dim);
}

.statsOverlay__close {
	padding: 2px 6px;
	font-size: 14px;
}

.statsOverlay__summary {
	display: flex;
	flex-wrap: wrap;
	gap: 4px 10px;
	margin: 8px 0;
	padding-bottom: 8px;
	border-bottom: 1px solid var(--border);
}

.statsOverlay__list {
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.statsOverlay__empty {
	margin: 4px 0;
	color: var(--text-dim);
}

.statsChip {
	display: inline-flex;
	gap: 4px;
	align-items: baseline;
	white-space: nowrap;
}

.statsChip__label {
	color: var(--text-dim);
	text-transform: uppercase;
	font-size: 10px;
	letter-spacing: 0.03em;
}

.statsRow {
	padding: 6px 8px;
	border: 1px solid transparent;
	border-left: 3px solid var(--success);
	border-radius: 8px;
	background: rgba(255, 255, 255, 0.04);
}

.statsRow[data-quality="ok"] {
	border-left-color: var(--warning);
}

.statsRow[data-quality="poor"] {
	border-left-color: var(--danger);
}

.statsRow[data-quality="unknown"] {
	border-left-color: var(--text-dim);
}

.statsRow.is-focused {
	border-color: var(--accent);
	background: var(--accent-soft);
	cursor: pointer;
}

.statsRow__head {
	display: flex;
	justify-content: space-between;
	gap: 8px;
}

.statsRow__name {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-weight: 600;
}

.statsRow__state {
	color: var(--text-dim);
}

.statsRow__values {
	display: flex;
	flex-wrap: wrap;
	gap: 2px 10px;
	margin-top: 3px;
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: 11px;
}

.statsRow__action {
	display: none;
	margin-top: 4px;
	padding: 2px 8px;
	font-size: 11px;
}

.statsRow:hover .statsRow__action {
	display: inline-block;
}

.fileProgress {
	position: absolute;
	left: 50%;
	bottom: 16px;
	transform: translateX(-50%);
	min-width: 260px;
	padding: 8px 12px;
	background: var(--bg-panel);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: var(--shadow);
	font-size: 13px;
}

.fileProgress__track {
	margin-top: 6px;
	height: 6px;
	border-radius: 3px;
	background: var(--bg);
	overflow: hidden;
}

.fileProgress__bar {
	height: 100%;
	width: 0;
	background: var(--accent);
	transition: width 0.2s ease;
}

.dropHint {
	position: absolute;
	inset: 12px;
	display: grid;
	place-items: center;
	border: 2px dashed var(--accent);
	border-radius: var(--radius);
	background: var(--accent-soft);
	font-size: 18px;
	pointer-events: none;
}

/* Toasts (spec §4): bottom-right on desktop, top on mobile, stacked,
   dismissible. `ui/toasts.js` only adds `.toast`, `.toast--<type>`, the
   message, the action buttons and the close button — the whole look is here. */

.toasts {
	position: fixed;
	/* Bottom-right so a toast never sits on top of the conversation the user
	   is reading, and clear of the rail and the home indicator. */
	bottom: calc(var(--sp-16) + env(safe-area-inset-bottom));
	right: calc(var(--sp-16) + env(safe-area-inset-right));
	display: flex;
	flex-direction: column;
	gap: var(--sp-8);
	z-index: 50;
	pointer-events: none;
}

.toast {
	display: flex;
	align-items: center;
	gap: var(--sp-12);
	padding: var(--sp-12) var(--sp-16);
	border-radius: var(--radius-lg);
	background: var(--bg-overlay);
	border: 1px solid var(--border-default);
	box-shadow: var(--shadow-3);
	font-size: var(--fs-14);
	max-width: 360px;
	pointer-events: auto;
	/* Enter from the edge it lives on; transform/opacity only (spec §8). */
	animation: toast-in var(--dur) var(--ease);
}

.toast--success {
	border-left: 3px solid var(--positive);
}

.toast--warning {
	border-left: 3px solid var(--caution);
}

.toast--error {
	border-left: 3px solid var(--critical);
}

.toast--info {
	border-left: 3px solid var(--info);
}

.toast__text {
	flex: 1;
	line-height: var(--lh-prose);
}

.toast__action {
	border: 0;
	background: var(--accent);
	color: var(--text-on-accent);
	border-radius: var(--radius-sm);
	padding: var(--sp-4) var(--sp-12);
	font: inherit;
	font-weight: 600;
	cursor: pointer;
	min-height: var(--control-h);
	transition: background var(--dur-fast) var(--ease);
}

.toast__action:hover {
	background: var(--accent-hover);
}

.toast__close {
	border: 0;
	background: transparent;
	color: var(--text-tertiary);
	font-size: var(--fs-20);
	line-height: 1;
	cursor: pointer;
	width: 32px;
	height: 32px;
	border-radius: var(--radius-sm);
	flex: none;
}

.toast__close:hover {
	background: var(--bg-hover);
	color: var(--text-primary);
}

.badge {
	display: inline-flex;
	align-items: center;
	gap: var(--sp-4);
	padding: var(--sp-2) var(--sp-8);
	border-radius: var(--radius-full);
	background: var(--bg-surface);
	border: 1px solid var(--border-default);
	font-size: var(--fs-12);
	line-height: 1.5;
	color: var(--text-secondary);
	white-space: nowrap;
}

.badge--warning {
	border-color: var(--caution);
	color: var(--caution);
	background: var(--caution-soft);
}

.badge--accent {
	border-color: var(--accent);
	color: var(--accent);
	background: var(--accent-soft);
}

/* Connection state (spec §7: colour is never the only signal — the label says
   the same thing in words, and the dot pulses while it is not settled). */
#connectionBadge::before {
	content: "";
	width: 6px;
	height: 6px;
	border-radius: var(--radius-full);
	background: currentColor;
	flex: none;
}

#connectionBadge[data-state="online"] {
	border-color: var(--positive);
	color: var(--positive);
	background: var(--positive-soft);
}

#connectionBadge[data-state="reconnecting"]::before,
#connectionBadge[data-state="connecting"]::before {
	animation: shell-pulse 1.6s var(--ease) infinite;
}

#connectionBadge[data-state="reconnecting"],
#connectionBadge[data-state="offline"] {
	border-color: var(--caution);
	color: var(--caution);
	background: var(--caution-soft);
}

#connectionBadge[data-state="offline"] {
	border-color: var(--critical);
	color: var(--critical);
	background: var(--critical-soft);
}

/* ---------------------------------------------------------------- mobile */

/* Short viewports (laptops in split screen, phones in landscape): keep the
   lobby usable without scrolling. */
@media (max-height: 720px) {
	.lobby__preview {
		aspect-ratio: 16 / 9;
		max-height: 34vh;
	}

	.lobby__card {
		padding: var(--sp-20);
	}

	.lobby__header {
		margin-bottom: var(--sp-12);
	}
}

@media (max-width: 640px) {
	.grid[data-layout="grid"] {
		--cols: 1 !important;
	}

	.grid[data-layout="speaker"] {
		--strip-cols: 2;
	}

	.grid[data-layout="sidebar"] {
		grid-template-columns: minmax(0, 1fr) clamp(110px, 34%, 180px);
	}

	.sidePanel {
		width: 100%;
	}

	.statsOverlay {
		left: 8px;
		right: 8px;
		top: 8px;
		width: auto;
		max-height: 50vh;
	}

	/* Call controls (see the reasoning above): the media buttons stay on the
	   first line, everything else — including #btnLeave — on the lines below,
	   so leaving the call can never be pushed out of the viewport. */
	.controls {
		gap: 6px 4px;
		padding: 8px max(8px, env(safe-area-inset-right)) calc(8px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
	}

	/* A zero-height, full-width flex item: it wraps the bar into two groups
	   without adding a line of its own. */
	.controls__spacer {
		flex: 1 0 100%;
		height: 0;
		min-height: 0;
		margin: 0;
	}

	/* 40 px keeps every button a usable tap target; 5 × 40 + 4 × 4 + 2 × 8 =
	   236 px, so the media row also fits a 320 px phone. */
	.btn--round {
		width: 40px;
		height: 40px;
		font-size: 16px;
	}

	.popover {
		right: 8px;
		left: 8px;
		min-width: 0;
	}

	#layoutMenu {
		right: 8px;
	}
}

/* Landscape phones and short windows: here *height* is the scarce resource, so
   the bar gives up the two-group split (one 34 px line instead of two 40 px
   ones) and stays on a single row for as long as the width allows:
   13 × 34 + 12 × 4 = 490 px, which fits 568 × 320 landscape (552 px of usable
   width) and 640 × 360 (624 px). Below ~490 px of width it wraps again, and
   wrapping is safe — the leave button is the last item of the flow. */
@media (max-height: 480px) and (orientation: landscape) {
	.controls {
		gap: 4px;
		padding-top: 6px;
		padding-bottom: calc(6px + env(safe-area-inset-bottom));
	}

	.controls__spacer {
		display: none;
	}

	.btn--round {
		width: 34px;
		height: 34px;
		font-size: 15px;
	}

	.popover--up {
		max-height: calc(100vh - 56px);
	}

	/* Two columns of shortcuts only where there is room for two. */
	@media (min-width: 560px) {
		.popover__list {
			columns: 2;
			column-gap: 18px;
		}
	}
}

/* Reduced motion (spec §2): every animation in this file is wrapped in one
   override, so no component has to think about it twice. The rail indicator
   keeps moving instantly rather than disappearing — the user still has to see
   which view is active. */
@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		transition: none !important;
		animation: none !important;
		animation-duration: 0.01ms !important;
		transition-duration: 0.01ms !important;
	}
}

/* ================================================================== shell */
/*
 * The app shell (spec §3): a 64 px activity rail, a 48 px top bar and the view
 * router between them — Chat (DMs), Meetings (the pre-join screen) and
 * Calendar. The call (`#call`) is a sibling that overlays the whole shell while
 * a meeting runs.
 *
 * Design intent: the chrome is the quietest thing on screen. Neutral surfaces,
 * one purposeful accent, and the only moving part in the rail is a 2 px
 * indicator that slides to the view you are in.
 */

html,
body {
	overflow: hidden; /* the shell panes scroll themselves, not the page */
}

#shell {
	display: grid;
	grid-template-columns: var(--rail-w) minmax(0, 1fr);
	height: 100%;
	/* `100dvh` where it is supported: the mobile tab bar has to sit above the
	   home indicator without the panes jumping when the URL bar collapses. */
	height: 100dvh;
}

.shell__main {
	display: flex;
	flex-direction: column;
	min-width: 0;
	min-height: 0;
}

.shell__content {
	flex: 1;
	min-height: 0;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	background: var(--bg-canvas);
}

.view {
	flex: 1;
	min-height: 0;
	display: flex;
	flex-direction: column;
}

/* ------------------------------------------------------------------- rail */

.rail {
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--list-gap);
	padding: var(--sp-8) 0;
	background: var(--rail-bg);
	color: var(--rail-fg);
	/* Mica over the canvas — never over the video (the call hides the shell
	   entirely, so this can only ever be over the app background). */
	padding-top: calc(var(--sp-8) + env(safe-area-inset-top));
	padding-bottom: calc(var(--sp-8) + env(safe-area-inset-bottom));
	overflow: hidden;
}

.rail__logo {
	width: 32px;
	height: 32px;
	display: grid;
	place-items: center;
	margin: var(--sp-4) 0 var(--sp-12);
	background: var(--accent);
	color: var(--text-on-accent);
	border-radius: var(--radius-md);
	font-weight: 700;
	font-size: var(--fs-16);
	line-height: 1;
	pointer-events: none;
	/* The app mark is the one permanently filled element of the rail, so it
	   never competes with the "you are here" indicator. */
	letter-spacing: 0;
}

/* A view button: 20 px icon in a 40 px hit area, label underneath, and room
   for an unread dot. */
.rail__btn {
	position: relative;
	z-index: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--sp-2);
	width: 56px;
	min-height: 48px;
	padding: var(--sp-6) var(--sp-2);
	border: 0;
	border-radius: var(--radius-md);
	background: transparent;
	color: var(--rail-fg);
	font: inherit;
	font-size: var(--fs-11);
	line-height: 1;
	cursor: pointer;
	transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.rail__btn svg {
	width: var(--icon-size);
	height: var(--icon-size);
	flex: none;
}

.rail__btn:hover {
	background: var(--bg-hover);
	color: var(--text-primary);
}

.rail__btn.is-active {
	/* Active is the accent icon + the indicator, not a filled blob (spec §3). */
	color: var(--accent);
	font-weight: 600;
	background: transparent;
}

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

/* The "you are here" indicator: one element for the whole rail, moved with a
   transform so the transition between two items is a slide, not a repaint of
   three buttons. `.rail--ready` is set by `ui/shell.js` once the first view is
   known, so it never animates from the wrong place on load. */
.rail__indicator {
	position: absolute;
	z-index: 0;
	left: 0;
	top: 0;
	width: 3px;
	height: 0;
	border-radius: 0 var(--radius-full) var(--radius-full) 0;
	background: var(--accent);
	opacity: 0;
	transform: translate3d(0, var(--rail-indicator-y, 0), 0);
	transition: transform var(--dur) var(--ease), height var(--dur) var(--ease), opacity var(--dur-fast) var(--ease);
	pointer-events: none;
}

.rail--ready .rail__indicator {
	opacity: 1;
}

/* Unread badge of the rail (DMs live here; room chat badges stay in the call).
   The number is in the element's text, so it is also the accessible name. */
.rail__dot {
	position: absolute;
	top: var(--sp-2);
	right: var(--sp-6);
	min-width: 16px;
	height: 16px;
	padding: 0 var(--sp-4);
	display: grid;
	place-items: center;
	background: var(--accent);
	color: var(--text-on-accent);
	border: 2px solid var(--bg-surface);
	border-radius: var(--radius-full);
	font-size: var(--fs-11);
	font-weight: 700;
	line-height: 1;
	font-style: normal;
	animation: shell-pop var(--dur) var(--ease);
}

.rail__dot:not([hidden]) {
	display: grid;
}

/* Meetings lights up when a room is live — real-time state, not decoration. */
.rail__btn--live .rail__pulse {
	display: block;
}

.rail__pulse {
	display: none;
	position: absolute;
	bottom: var(--sp-6);
	left: 50%;
	transform: translateX(-50%);
	width: 6px;
	height: 6px;
	border-radius: var(--radius-full);
	background: var(--positive);
	animation: shell-pulse 2.4s var(--ease) infinite;
}

.rail__spacer {
	flex: 1;
}

/* The account button lives in the top bar in the markup (so it is in the
   right place on a phone) and is pinned to the foot of the rail on desktop.
   One node, one focus target, both layouts. */
.rail__user {
	position: fixed;
	z-index: 25;
	left: 0;
	bottom: calc(var(--sp-12) + env(safe-area-inset-bottom));
	width: var(--rail-w);
	border: 0;
	padding: 0;
	background: transparent;
	cursor: pointer;
	/* 40 px of hit area around a 32 px avatar: a 32 px target is a miss on
	   touch, and the avatar has to feel like a button. */
	min-width: 40px;
	min-height: 40px;
	display: grid;
	place-items: center;
	border-radius: var(--radius-full);
}

.rail__user:hover .rail__avatar {
	box-shadow: 0 0 0 2px var(--bg-surface), 0 0 0 4px var(--accent);
}

.rail__avatar {
	display: grid;
	place-items: center;
	width: 32px;
	height: 32px;
	background: var(--bg-accent-soft);
	color: var(--accent);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-full);
	font-size: var(--fs-14);
	font-weight: 600;
	transition: box-shadow var(--dur-fast) var(--ease);
}

/* ----------------------------------------------------------------- topbar */

.topbar {
	display: flex;
	align-items: center;
	gap: var(--sp-12);
	height: var(--topbar-h);
	padding: 0 var(--sp-16) 0 var(--sp-20);
	background: var(--rail-bg);
	border-bottom: 1px solid var(--border-subtle);
	position: relative;
	z-index: 20;
	flex: none;
}

.topbar__title {
	margin: 0;
	font-size: var(--fs-16);
	line-height: var(--lh-heading);
	font-weight: 600;
	color: var(--text-primary);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.topbar__spacer {
	flex: 1;
}

/* The search field grows into whatever space is left between the title and
   the account cluster, and stops growing at a readable measure. */
.topbar__search {
	position: relative;
	display: flex;
	align-items: center;
	flex: 1 1 auto;
	min-width: 0;
	max-width: 420px;
	margin-inline: auto;
}

.topbar__searchIcon {
	position: absolute;
	left: var(--sp-8);
	display: grid;
	place-items: center;
	color: var(--text-tertiary);
	pointer-events: none;
}

.topbar__searchIcon svg {
	width: var(--icon-size);
	height: var(--icon-size);
}

.topbar__searchInput {
	width: 100%;
	min-width: 0;
	height: 32px;
	/* Room for the icon on the left and for the ⌘K hint (or the clear button) on
	   the right. */
	padding: 0 var(--sp-16) 0 var(--sp-32);
	border: 1px solid transparent;
	border-bottom: 1px solid var(--border-strong);
	border-radius: var(--radius-sm) var(--radius-sm) 0 0;
	background: var(--bg-elevated);
	color: var(--text-primary);
	font: inherit;
	font-size: var(--fs-13);
	transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.topbar__searchInput::placeholder {
	color: var(--text-tertiary);
}

.topbar__searchInput:hover {
	background: var(--bg-hover);
}

/* Fluent's search field goes "active" (accent border, filled background)
   instead of showing a hard focus ring around a big box. */
.topbar__search:focus-within {
	border-color: var(--accent);
	border-bottom-width: 2px;
	background: var(--bg-elevated);
}

.topbar__search:focus-within .topbar__searchIcon {
	color: var(--accent);
}

/* The ⌘K hint disappears the moment the field has focus — it is a hint, not
   chrome. */
.topbar__kbd {
	position: absolute;
	right: var(--sp-8);
	display: inline-flex;
	gap: var(--sp-2);
	pointer-events: none;
}

.topbar__kbd kbd {
	padding: 1px var(--sp-4);
	border: 1px solid var(--border-subtle);
	border-radius: var(--radius-sm);
	background: var(--bg-hover);
	color: var(--text-tertiary);
	font: inherit;
	font-size: var(--fs-11);
	line-height: 1.4;
}

.topbar__search:focus-within .topbar__kbd,
.topbar__search--filled .topbar__kbd {
	display: none;
}

.topbar__searchClear {
	position: absolute;
	right: var(--sp-2);
	display: none;
	width: 28px;
	height: 28px;
	border: 0;
	border-radius: var(--radius-sm);
	background: transparent;
	color: var(--text-secondary);
	font-size: var(--fs-16);
	line-height: 1;
	cursor: pointer;
}

.topbar__search--filled .topbar__searchClear {
	display: grid;
	place-items: center;
}

.topbar__searchClear:hover {
	background: var(--bg-hover);
	color: var(--text-primary);
}

/* Our own presence: a ringed dot + name. Colour is not the only signal — the
   title and the `aria-label` say the same thing. */
.topbar__user {
	display: inline-flex;
	align-items: center;
	gap: var(--sp-6);
	min-height: var(--control-h);
	padding: 0 var(--sp-8);
	border-radius: var(--radius-md);
	font-size: var(--fs-13);
	color: var(--text-secondary);
	white-space: nowrap;
}

.topbar__user::before {
	content: "";
	width: 8px;
	height: 8px;
	flex: none;
	border-radius: var(--radius-full);
	background: var(--text-tertiary);
}

.topbar__user[data-online="yes"]::before {
	background: var(--positive);
}

.topbar__user[data-online="yes"] {
	color: var(--text-primary);
}

/* "Join room" button in the top bar: the entry point to a new call. */
.topbar__meet {
	padding: var(--sp-6) var(--sp-16);
	font-size: var(--fs-13);
	flex: none;
	min-height: var(--control-h);
}

/* Landing on a view is announced without moving focus (spec §7). */
.topbar__status {
	white-space: nowrap;
}

/* -------------------------------------------------- lobby inside the shell */

#lobby.lobby {
	padding: var(--sp-16);
	overflow: auto;
}

/* ---------------------------------------------------- shell: keyframes */

@keyframes shell-pop {
	from {
		transform: scale(0.4);
	}

	to {
		transform: scale(1);
	}
}

@keyframes shell-pulse {
	0%,
	100% {
		opacity: 1;
	}

	50% {
		opacity: 0.35;
	}
}

@keyframes toast-in {
	from {
		opacity: 0;
		transform: translateY(var(--sp-8));
	}

	to {
		opacity: 1;
		transform: translateY(0);
	}
}

/* ----------------------------------------------------- dm page (chat view) */

#chatView {
	flex-direction: row;
}

.dm__listPane {
	display: flex;
	flex-direction: column;
	width: min(340px, 38%);
	min-width: 260px;
	border-right: 1px solid var(--border);
	background: var(--bg);
}

.dm__threadPane {
	flex: 1;
	display: flex;
	flex-direction: column;
	min-width: 0;
	background: var(--bg-elevated);
}

.dm__find {
	padding: 12px 12px 8px;
}

.dm__findInput {
	margin-bottom: 6px;
}

.dm__feedback {
	margin: 0 0 8px;
	font-size: 12px;
	color: var(--success);
}

.dm__feedback--error {
	color: var(--danger);
}

.dm__list {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	padding: 4px 8px 12px;
}

/* Every row (contact or room) is a wrapper div; the grid cells live on the
   inner .dm__rowMain, and the trailing column holds the Join/Call action. */
.dm__row {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	gap: 6px;
	align-items: center;
	width: 100%;
	padding: 2px 0;
	border: 0;
	border-radius: 8px;
	background: transparent;
	color: var(--text);
	font: inherit;
	text-align: left;
}

.dm__row:hover {
	background: var(--hover);
}

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

.dm__row--unread {
	background: var(--accent-soft);
}

/* Contact rows carry a Call action like room rows carry a Join action, so
   the row is a wrapper div and the grid moves onto the inner .dm__rowMain. */
.dm__row .dm__rowMain {
	display: grid;
	grid-template-columns: 40px minmax(0, 1fr) auto;
	gap: 10px;
	align-items: center;
	min-width: 0;
	border: 0;
	border-radius: 8px;
	background: transparent;
	color: var(--text);
	font: inherit;
	text-align: left;
	cursor: pointer;
	padding: 9px 10px;
	width: 100%;
}

.dm__row .dm__rowMain:hover,
.dm__row .dm__rowMain:focus-visible {
	background: var(--hover);
}

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

/* The thread that is open right now stays highlighted in the list. */
.dm__row--active .dm__rowMain,
.dm__row--active {
	background: var(--accent-soft);
}

.dm__row--active .dm__rowName {
	color: var(--accent-strong);
}

.dm__avatar {
	position: relative;
	display: grid;
	place-items: center;
	width: 40px;
	height: 40px;
	border-radius: 50%;
	background: var(--accent-strong);
	color: #fff;
	font-weight: 600;
}

/* Presence dot: solid when the contact currently has a live session. */
.dm__avatar[data-online="yes"]::after {
	content: "";
	position: absolute;
	right: -1px;
	bottom: -1px;
	width: 11px;
	height: 11px;
	border-radius: 50%;
	background: var(--success);
	border: 2px solid var(--bg);
}

.dm__rowText {
	min-width: 0;
}

.dm__rowName {
	display: block;
	font-weight: 600;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.dm__rowPreview {
	display: block;
	color: var(--text-dim);
	font-size: 12px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.dm__row--unread .dm__rowPreview {
	color: var(--text);
}

.dm__rowMeta {
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: 5px;
}

.dm__rowTime {
	color: var(--text-dim);
	font-size: 11px;
	white-space: nowrap;
}

.dm__unread {
	display: grid;
	place-items: center;
	min-width: 18px;
	height: 18px;
	padding: 0 5px;
	background: var(--accent);
	color: #fff;
	border-radius: 9px;
	font-size: 10.5px;
	font-weight: 700;
}

/* Room row headcount: ● people in the call right now, ○ members who know the
   room but are not in it. Sits above the timestamp in the meta column. */
.dm__rowCounts {
	display: inline-flex;
	gap: 5px;
	font-size: 10.5px;
	font-weight: 700;
	white-space: nowrap;
}

.dm__count {
	padding: 1px 5px;
	border-radius: 8px;
	background: var(--bg-elevated);
	color: var(--text-dim);
	font-variant-numeric: tabular-nums;
}

.dm__count--active {
	background: var(--accent-soft);
	color: var(--accent-strong);
}

.dm__count--passive {
	background: transparent;
}

/* Room rows: a wrapper div (invalid to nest buttons), so the .dm__row grid
   lives on the inner .dm__rowMain and the Join action becomes a third column. */
.dm__row--room {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	gap: 6px;
	padding: 4px 4px 4px 0;
	border-radius: 8px;
}

.dm__row--room .dm__rowMain {
	display: grid;
	grid-template-columns: 40px minmax(0, 1fr) auto;
	gap: 10px;
	align-items: center;
	min-width: 0;
	border: 0;
	border-radius: 6px;
	background: transparent;
	color: var(--text);
	font: inherit;
	text-align: left;
	cursor: pointer;
	padding: 5px 6px 5px 10px;
}

.dm__row--room:hover .dm__rowMain,
.dm__row--room .dm__rowMain:focus-visible {
	background: var(--hover);
}

.dm__row--room .dm__rowMain:focus-visible {
	outline: 2px solid var(--accent);
}

/* The room avatar is a "#" badge instead of a name letter. */
.dm__avatar--room {
	background: var(--accent);
	border-radius: 10px;
	font-size: 17px;
}

/* Small "Join"/"Call" action right in the row. */
.dm__rowJoin,
.dm__rowCall {
	border: 1px solid var(--border);
	border-radius: 6px;
	background: transparent;
	color: var(--text);
	font: inherit;
	font-size: 12px;
	font-weight: 600;
	padding: 4px 10px;
	cursor: pointer;
	white-space: nowrap;
}

.dm__rowJoin:hover,
.dm__rowCall:hover {
	background: var(--accent-soft);
	color: var(--accent-strong);
}

.dm__join {
	margin-left: auto;
	flex: none;
}

.dm__threadHeader .dm__join {
	padding: 5px 14px;
	font-size: 13px;
}

.dm__threadHeader {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 8px 14px;
	border-bottom: 1px solid var(--border);
	background: var(--bg);
}

.dm__threadHeader .dm__avatar {
	width: 32px;
	height: 32px;
	font-size: 13px;
}

.dm__threadHeader .dm__avatar[data-online="yes"]::after {
	border-color: var(--bg);
}

.dm__threadTitle {
	min-width: 0;
	display: flex;
	flex-direction: column;
}

.dm__threadTitle strong {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.dm__threadTitle small {
	color: var(--text-dim);
	font-size: 11px;
}

.dm__thread {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
}

/* Roster of the open room's call, above the thread: who is in the room right
   now and what their media is doing, without joining the call. */
.dm__roster {
	flex: none;
	max-height: 34%;
	overflow-y: auto;
	padding: 4px 0 8px;
	border-bottom: 1px solid var(--border);
	background: var(--bg);
}

.dm__roster[hidden] {
	display: none;
}

.dm__roster .panel__title {
	margin: 6px 0 2px;
}

.dm__roster .row {
	padding: 5px 0;
	border-bottom: 0;
}

.dm__roster .row__avatar {
	width: 28px;
	height: 28px;
	font-size: 12px;
}

.dm__thread .bubble {
	max-width: min(560px, 75%);
}

.dm__empty {
	margin: 48px auto;
	max-width: 380px;
	text-align: center;
	padding: 0 16px;
}

.dm__emptyTitle {
	margin: 0 0 6px;
	font-size: 18px;
	font-weight: 600;
}

.dm__back {
	padding: 4px 8px;
	font-size: 16px;
}

/* --------------------------------------------------------- calendar view */

#calView {
	overflow: auto;
}

.cal__panes {
	display: grid;
	grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr);
	gap: 16px;
	padding: 16px;
	width: 100%;
	max-width: 1200px;
	margin: 0 auto;
	height: max-content;
}

.cal__head {
	display: flex;
	align-items: center;
	gap: 6px;
	margin-bottom: 10px;
}

.cal__month {
	font-size: 18px;
	font-weight: 600;
	margin-inline: 6px;
}

.cal__spacer {
	flex: 1;
}

.cal__grid {
	display: grid;
	grid-template-columns: repeat(7, minmax(0, 1fr));
	gap: 4px;
}

.cal__weekday {
	text-align: center;
	padding: 2px 0 6px;
	font-size: 10.5px;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	color: var(--text-dim);
}

.cal__cell {
	display: flex;
	flex-direction: column;
	gap: 3px;
	min-height: 88px;
	padding: 4px;
	background: var(--bg-elevated);
	border: 1px solid var(--border);
	border-radius: var(--radius-sm);
	overflow: hidden;
}

.cal__cell--outside {
	opacity: 0.45;
}

.cal__cell--today {
	border-color: var(--accent);
	box-shadow: inset 0 0 0 1px var(--accent);
}

.cal__cellDay {
	font-size: 11.5px;
	color: var(--text-dim);
	font-variant-numeric: tabular-nums;
}

.cal__chip {
	width: 100%;
	padding: 1px 6px;
	border: 0;
	border-radius: 4px;
	background: var(--accent-soft);
	color: var(--accent);
	font-size: 11px;
	line-height: 1.6;
	text-align: left;
	cursor: pointer;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.cal__chip--live {
	background: var(--success);
	color: #fff;
}

.cal__more {
	font-size: 10px;
	color: var(--text-dim);
}

.cal__upcoming {
	min-width: 0;
}

.cal__event {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: 6px;
	padding: 10px 12px;
	margin-bottom: 8px;
	background: var(--bg-elevated);
	border: 1px solid var(--border);
	border-radius: 10px;
}

.cal__eventTime {
	font-size: 12px;
	color: var(--text-dim);
}

.cal__eventBody {
	display: flex;
	flex-direction: column;
	gap: 2px;
	min-width: 0;
}

.cal__eventTitle {
	font-weight: 600;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.cal__eventNotes {
	font-size: 12px;
	color: var(--text-dim);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.cal__eventActions {
	display: flex;
	justify-content: flex-end;
}

.cal__creator {
	margin-top: 4px;
	padding: 12px 14px;
	background: var(--bg-elevated);
	border: 1px solid var(--border);
	border-radius: 10px;
}

.cal__row {
	display: flex;
	gap: 10px;
}

.cal__row .field {
	flex: 1;
	min-width: 0;
}

.cal__creatorActions {
	display: flex;
	justify-content: flex-end;
	align-items: center;
	gap: 8px;
	margin-top: 10px;
}

.cal__creatorActions .cal__feedback {
	flex: 1;
}

.cal__feedback {
	margin: 0;
	font-size: 12.5px;
	color: var(--danger);
}

/* -------------------------------------------------- account popover (rail) */

/* Anchored to the account button: bottom-left on the desktop rail, top-right
   in the top bar on a phone. Both positions are declared here (and overridden
   in the mobile block below) so `ui/shell.js` only has to flip `hidden`. */
.popover--account {
	position: fixed;
	top: auto;
	bottom: calc(var(--sp-12) + env(safe-area-inset-bottom));
	left: calc(var(--rail-w) + var(--sp-8));
	right: auto;
	z-index: 60;
	width: 320px;
	max-width: calc(100vw - var(--rail-w) - var(--sp-16));
	max-height: calc(100vh - var(--sp-24));
	overflow-y: auto;
	overscroll-behavior: contain;
	/* The popover already paints `--bg-overlay`; `mica` layers the translucent
	   tint on top of it (spec §2), with the opaque overlay as its fallback. */
	animation: fade-in var(--dur-fast) var(--ease);
}

/* The name and the hint above the form. */
.popover--account > p {
	margin: 0 0 var(--sp-6);
}

.popover--account > p strong {
	display: block;
	font-size: var(--fs-14);
	color: var(--text-primary);
}

.popover__hint {
	margin: 0;
	font-size: var(--fs-12);
	line-height: var(--lh-prose);
	color: var(--text-secondary);
}

.accountForm {
	margin-top: var(--sp-12);
	padding-top: var(--sp-12);
	border-top: 1px solid var(--border-subtle);
}

/* Create account / sign in switch. A segmented control, not two buttons: the
   two are alternatives of one action, so they share one control. */
.accountForm__mode {
	display: flex;
	gap: var(--sp-4);
	margin-bottom: var(--sp-12);
	padding: var(--sp-2);
	background: var(--bg-canvas);
	border-radius: var(--radius-md);
}

.accountForm__mode button {
	flex: 1;
	min-height: 28px;
	border: 1px solid transparent;
	border-radius: var(--radius-sm);
	background: transparent;
	color: var(--text-secondary);
	font: inherit;
	font-size: var(--fs-13);
	font-weight: 600;
	padding: var(--sp-4) 0;
	cursor: pointer;
	transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.accountForm__mode button:hover {
	color: var(--text-primary);
}

.accountForm__mode button.is-active {
	background: var(--bg-elevated);
	border-color: var(--border-default);
	color: var(--text-primary);
	box-shadow: var(--shadow-1);
}

.accountForm #accountSignOut,
.popover--account #accountSignOut {
	margin-top: var(--sp-8);
}

/* --------------------------------------------------------- shell: mobile */

/* Below 640 px the rail is a bottom tab bar and the account button belongs in
   the top bar (spec §6). The DOM does not change: `.rail__user` is `position:
   fixed` over the rail on desktop and returns to the flow here, so there is
   exactly one account button (and one focus target) in both layouts. */
@media (max-width: 640px) {
	#shell {
		grid-template-columns: minmax(0, 1fr);
		grid-template-rows: minmax(0, 1fr) auto;
	}

	.shell__main {
		order: 1;
		min-height: 0;
	}

	.rail {
		order: 2;
		flex-direction: row;
		align-items: stretch;
		width: 100%;
		height: auto;
		/* 56 px of bar plus whatever the home indicator claims. */
		padding: var(--sp-4) max(var(--sp-4), env(safe-area-inset-right)) calc(var(--sp-4) + env(safe-area-inset-bottom)) max(var(--sp-4), env(safe-area-inset-left));
		gap: 0;
		border-top: 1px solid var(--border-subtle);
		overflow: visible;
	}

	.rail__logo,
	.rail__spacer {
		display: none;
	}

	.rail__btn {
		flex: 1 1 0;
		width: auto;
		/* A tab bar target, not a desktop one: 44 px minimum, centred label. */
		min-height: 48px;
		padding: var(--sp-4) var(--sp-2);
	}

	.rail__btn.is-active {
		color: var(--accent);
	}

	/* On a tab bar the indicator runs along the top edge and spans the item:
	   a 3 px sliver on the left of a full-width tab reads as nothing. */
	.rail__indicator {
		left: 50%;
		top: 0;
		width: 24px;
		height: 3px;
		border-radius: 0 0 var(--radius-full) var(--radius-full);
		transform: translate3d(calc(-50% + var(--rail-indicator-x, 0px)), 0, 0);
	}

	.rail__dot {
		top: var(--sp-2);
		right: 50%;
		margin-right: -22px;
	}

	.rail__pulse {
		bottom: var(--sp-4);
	}

	.rail__user {
		/* Back into the top bar's flow: this is where it belongs on a phone
		   (spec §6), and where the hand already is. */
		position: static;
		min-width: 44px;
		min-height: 44px;
		margin: 0;
		order: 6;
	}

	.rail__avatar {
		width: 28px;
		height: 28px;
		font-size: var(--fs-12);
	}

	/* The top bar carries the search below the title row on a phone: two rows
	   of 48 px would eat the conversation. */
	.topbar {
		height: auto;
		min-height: var(--topbar-h);
		flex-wrap: wrap;
		gap: var(--sp-8);
		padding: var(--sp-6) var(--sp-12);
		padding-top: calc(var(--sp-6) + env(safe-area-inset-top));
		padding-right: calc(var(--sp-12) + env(safe-area-inset-right));
		padding-left: calc(var(--sp-12) + env(safe-area-inset-left));
	}

	.topbar__title {
		flex: 1 1 auto;
		font-size: var(--fs-16);
	}

	.topbar__search {
		order: 5;
		flex: 1 0 100%;
		max-width: none;
		margin: 0;
	}

	.topbar__searchInput {
		height: 36px;
	}

	/* "Connected" is a status, not a label that has to fit: the dot says it
	   visually and the accessible name says it in words. */
	.topbar__user {
		display: none;
	}

	.topbar__meet {
		padding: var(--sp-6) var(--sp-12);
	}

	/* DM page: full-screen list to start; an open thread slides over it. */
	#chatView .dm__listPane {
		width: 100%;
		min-width: 0;
	}

	#chatView .dm__listPane.is-open {
		display: none;
	}

	#chatView .dm__threadPane {
		display: none;
	}

	#chatView .dm__threadPane.is-open {
		position: absolute;
		inset: 0;
		z-index: 5;
		display: flex;
		animation: thread-in var(--dur) var(--ease);
	}

	.cal__panes {
		grid-template-columns: minmax(0, 1fr);
	}

	/* The account popover hangs below the top bar; the mobile block below moves it
   to the top-right corner. */
	.popover--account {
		top: calc(var(--topbar-h) + env(safe-area-inset-top) + var(--sp-4));
		bottom: auto;
		left: auto;
		right: var(--sp-8);
		width: min(320px, calc(100vw - var(--sp-16)));
		max-width: none;
		max-height: calc(100vh - var(--topbar-h) - var(--sp-24));
	}

	/* Toasts go to the top on a phone: the bottom of the screen is the tab
	   bar and the thumb. */
	.toasts {
		top: calc(var(--sp-12) + env(safe-area-inset-top));
		bottom: auto;
		left: var(--sp-12);
		right: var(--sp-12);
		align-items: stretch;
	}

	.toast {
		max-width: none;
	}
}

/* The thread pushes in from the right (spec §6). Transform only, so it never
   reflows the list behind it. */
@keyframes thread-in {
	from {
		transform: translateX(var(--sp-16));
		opacity: 0;
	}

	to {
		transform: translateX(0);
		opacity: 1;
	}
}

@media (min-width: 641px) {
	/* Thread never overlays the list on desktop. */
	#chatView .dm__threadPane.is-open {
		position: static;
	}
}

/* ============================================================
   Account + interface settings surface (outside a call)

   New in one block: the "Interface settings" entry of the account
   popover and the dialog it opens. The dialog borrows the settings
   section of the call's side panel (the same DOM node is moved in
   and out by App._openSettingsDialog), so these rules only have to
   describe the *frame* around it — the controls themselves keep the
   look they have in the call.

   Colours are CSS variables only, so both palettes apply unchanged.
   ============================================================ */

.accountSettings {
	display: flex;
	align-items: center;
	gap: var(--sp-12);
	width: 100%;
	margin-top: var(--sp-12);
	padding: var(--sp-8) var(--sp-12);
	min-height: var(--row-h);
	border: 0;
	border-top: 1px solid var(--border-subtle);
	border-radius: 0;
	background: transparent;
	color: var(--text-primary);
	font: inherit;
	text-align: left;
	cursor: pointer;
	transition: background var(--dur-fast) var(--ease);
}

.accountSettings:hover {
	background: var(--bg-hover);
}

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

.accountSettings svg {
	flex: none;
	width: var(--icon-size);
	height: var(--icon-size);
	color: var(--text-secondary);
}

.accountSettings span {
	display: flex;
	flex-direction: column;
	font-size: var(--fs-13);
	font-weight: 600;
	line-height: 1.3;
}

.accountSettings small {
	font-size: var(--fs-12);
	font-weight: 400;
	color: var(--text-secondary);
}

/* Modal frame (spec §4). `hidden` does the hiding; no `display` here that
   could outrank it. */
.dialog {
	position: fixed;
	inset: 0;
	z-index: 90;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--sp-20);
	background: var(--scrim);
	animation: fade-in var(--dur-fast) var(--ease);
}

/* The drag handle only exists on the mobile bottom-sheet form below. */
.dialog__grip {
	display: none;
}

.dialog__box {
	display: flex;
	flex-direction: column;
	width: min(480px, 100%);
	max-height: 100%;
	background: var(--bg-overlay);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-xl);
	box-shadow: var(--shadow-3);
	overflow: hidden;
	animation: dialog-in var(--dur) var(--ease);
}

.dialog__header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sp-12);
	padding: var(--sp-16) var(--sp-16) var(--sp-12) var(--sp-24);
	border-bottom: 1px solid var(--border-subtle);
	flex: none;
}

.dialog__title {
	margin: 0;
	font-size: var(--fs-20);
	line-height: var(--lh-heading);
	letter-spacing: var(--ls-display);
	font-weight: 600;
	color: var(--text-primary);
}

.dialog__header .btn--ghost {
	width: 32px;
	height: 32px;
	padding: 0;
	font-size: var(--fs-16);
}

/* The borrowed `.settings` section scrolls inside the dialog; this wrapper only
   has to give it a bounded flex column to scroll in. */
.dialog__body {
	display: flex;
	flex-direction: column;
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	overscroll-behavior: contain;
}

@keyframes dialog-in {
	from {
		opacity: 0;
		transform: scale(0.97);
	}

	to {
		opacity: 1;
		transform: scale(1);
	}
}

@media (max-width: 640px) {
	/* A bottom sheet, not a centred box (spec §6): the settings have to be
	   readable full width, and the grip tells the user it is a layer. */
	.dialog {
		padding: 0;
		align-items: flex-end;
	}

	.dialog__box {
		width: 100%;
		max-height: 92dvh;
		border: 0;
		border-radius: var(--radius-xl) var(--radius-xl) 0 0;
	}

	.dialog__grip {
		display: block;
		flex: none;
		width: 36px;
		height: 4px;
		margin: var(--sp-8) auto 0;
		border-radius: var(--radius-full);
		background: var(--border-strong);
	}

	.dialog__header {
		padding-top: var(--sp-12);
	}
}

/* ==================================================================== chat
 *
 * The chat surface: the conversation list, the thread, the composer and the
 * in-call chat panel. One block, because they are one component family —
 * `ui/chatParts.js` renders the same bubbles and the same composer into both,
 * and a message must not look or behave differently depending on where it is
 * read (spec §4).
 *
 * Rules above still describe the older, plainer version of these components;
 * everything below restates them against the token layer, and this block wins
 * by order for equal specificity. Only `.dm__*`, `.chat__*` and `.bubble*` are
 * touched here — no token, no rail, no top bar, no dialog.
 *
 * Design notes worth keeping:
 *   - A row is a grid of avatar / text / meta. The meta column is the only
 *     place a number appears, so the eye can scan one column for "what needs
 *     me".
 *   - `is-live` (a room with people in it) is a wash, not a badge: it answers
 *     "where is the meeting?" without adding a second coloured thing.
 *   - The composer is pinned and never grows past `--dm-composer-max`; the
 *     textarea scrolls instead, so the message list keeps its height.
 */

/* --- chat-scoped aliases: everything below reads these ------------------- */
.dm__listPane,
.dm__threadPane,
.chat {
	--dm-composer-max: 160px;
	--dm-row-h: var(--row-h);
	--dm-pane-pad: var(--sp-16);
}

/* ================================================================= list pane */

.dm__listPane {
	display: flex;
	flex-direction: column;
	width: min(360px, 38%);
	min-width: 288px;
	min-height: 0;
	background: var(--bg-surface);
	border-right: var(--sp-4) solid var(--border-subtle);
	border-right-width: 1px;
	overflow: hidden;
}

.dm__find {
	padding: var(--sp-12) var(--sp-12) var(--sp-4);
}

.dm__findInput {
	margin-bottom: var(--sp-4);
}

/* Filter row: sticky, because losing the search box halfway down a long list
   is what makes a filter feel broken. */
.dm__filter {
	position: sticky;
	top: 0;
	z-index: 2;
	display: flex;
	align-items: center;
	gap: var(--sp-4);
	padding: var(--sp-4) var(--sp-12) var(--sp-8);
	background: var(--bg-surface);
}

.dm__filterInput {
	flex: 1;
	min-width: 0;
	height: var(--control-h);
	padding: 0 var(--sp-8);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-sm);
	background: var(--bg-elevated);
	color: var(--text-primary);
	font: inherit;
	font-size: var(--fs-13);
}

.dm__filterInput::placeholder {
	color: var(--text-tertiary);
}

.dm__filterInput:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: -1px;
	border-color: var(--accent);
}

.dm__filterClear {
	flex: none;
	width: var(--control-h);
	height: var(--control-h);
	border: 0;
	border-radius: var(--radius-full);
	background: transparent;
	color: var(--text-secondary);
	font: inherit;
	font-size: var(--fs-12);
	cursor: pointer;
}

.dm__filterClear:hover {
	background: var(--bg-hover);
	color: var(--text-primary);
}

.dm__list {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	overscroll-behavior: contain;
	padding: 0 var(--sp-8) var(--sp-12);
	scrollbar-gutter: stable;
}

/* Off-screen, still announced: unread counts and headcounts change on their
   own and must be readable without sight (spec §7). */
.dm__srLive {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* --- sections ----------------------------------------------------------- */

.dm__section + .dm__section {
	margin-top: var(--sp-8);
}

/* Safety net for any filter that hides rows directly (the shell's top-bar
   search does): a section whose rows are all hidden goes with them. */
.dm__section:not(:has(.dm__row:not([hidden]))) {
	display: none;
}

.dm__sectionTitle {
	margin: 0;
	padding: var(--sp-8) var(--sp-8) var(--sp-4);
	color: var(--text-tertiary);
	font-size: var(--fs-11);
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
}

.dm__sectionRows {
	display: flex;
	flex-direction: column;
	gap: var(--sp-2);
}

/* --- the row ------------------------------------------------------------ */

.dm__row {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	align-items: center;
	gap: var(--sp-4);
	width: 100%;
	padding: var(--sp-2) var(--sp-4) var(--sp-2) var(--sp-2);
	border: 0;
	border-radius: var(--radius-md);
	background: transparent;
	color: var(--text-primary);
	font: inherit;
	text-align: left;
	transition: background var(--dur-fast) var(--ease);
}

.dm__row:hover {
	background: var(--bg-hover);
}

/* A room with people in it: a wash, so "where is the meeting" is answered
   without a second saturated colour competing with the unread pill. */
.dm__row--live {
	background: linear-gradient(90deg, var(--accent-soft), transparent 65%);
}

.dm__row--live:hover {
	background: var(--bg-hover);
}

.dm__row--unread {
	background: var(--bg-selected);
}

.dm__row--unread.dm__row--live {
	background: linear-gradient(90deg, var(--accent-soft), var(--bg-selected) 65%);
}

.dm__row--active {
	background: var(--bg-selected);
	box-shadow: inset 2px 0 0 var(--accent);
}

.dm__row:focus-within {
	background: var(--bg-hover);
}

.dm__row .dm__rowMain {
	display: grid;
	grid-template-columns: var(--dm-row-h) minmax(0, 1fr);
	column-gap: var(--sp-12);
	align-items: center;
	min-width: 0;
	padding: var(--sp-4);
	border: 0;
	border-radius: var(--radius-md);
	background: transparent;
	color: inherit;
	font: inherit;
	text-align: left;
	cursor: pointer;
}

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

.dm__avatar {
	grid-area: avatar;
	position: relative;
	display: grid;
	place-items: center;
	width: var(--dm-row-h);
	height: var(--dm-row-h);
	border-radius: var(--radius-full);
	background: var(--accent);
	color: var(--text-on-accent);
	font-size: var(--fs-13);
	font-weight: 600;
}

/* Presence dot: bottom-right, ringed in the row background so it reads as a
   separate object rather than a blemish on the avatar. */
.dm__avatar[data-online="yes"]::after {
	content: "";
	position: absolute;
	right: -1px;
	bottom: -1px;
	width: var(--sp-12);
	height: var(--sp-12);
	border-radius: var(--radius-full);
	background: var(--positive);
	border: 2px solid var(--bg-surface);
}

.dm__row--active .dm__avatar[data-online="yes"]::after {
	border-color: var(--bg-surface);
}

.dm__avatar--room {
	border-radius: var(--radius-lg);
	background: var(--accent-soft);
	color: var(--accent);
	font-size: var(--fs-16);
}

.dm__rowText {
	grid-area: text;
	min-width: 0;
}

.dm__rowTop {
	display: flex;
	align-items: baseline;
	gap: var(--sp-8);
	min-width: 0;
}

.dm__rowName {
	flex: 1;
	min-width: 0;
	font-size: var(--fs-14);
	font-weight: 400;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.dm__row--unread .dm__rowName {
	font-weight: 600;
}

.dm__row--active .dm__rowName {
	color: var(--accent);
}

.dm__rowPreview {
	display: block;
	font-size: var(--fs-12);
	color: var(--text-secondary);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.dm__row--unread .dm__rowPreview {
	color: var(--text-primary);
}

.dm__rowMeta {
	flex: none;
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: var(--sp-4);
}

.dm__rowTime {
	color: var(--text-tertiary);
	font-size: var(--fs-11);
	white-space: nowrap;
}

.dm__unread {
	display: grid;
	place-items: center;
	min-width: var(--sp-16);
	height: var(--sp-16);
	padding: 0 var(--sp-6);
	border-radius: var(--radius-full);
	background: var(--accent);
	color: var(--text-on-accent);
	font-size: var(--fs-11);
	font-weight: 700;
	font-variant-numeric: tabular-nums;
}

/* Room row headcount: ● people in the call right now, ○ members who know the
   room but are not in it. Sits above the timestamp in the meta column. */
.dm__rowCounts {
	display: inline-flex;
	gap: var(--sp-4);
	font-size: var(--fs-11);
	font-weight: 600;
	white-space: nowrap;
}

.dm__count {
	padding: 0 var(--sp-4);
	border-radius: var(--radius-sm);
	background: var(--bg-elevated);
	color: var(--text-tertiary);
	font-variant-numeric: tabular-nums;
}

.dm__count--active {
	background: var(--accent-soft);
	color: var(--accent);
}

.dm__row--live .dm__count--active {
	background: var(--positive-soft);
	color: var(--positive);
}

/* Trailing row action. Hidden until the row is hovered or something inside it
   has focus — but never on touch, and never smaller than a target. */
.dm__rowJoin,
.dm__rowCall {
	justify-self: end;
	align-self: center;
	min-height: var(--control-h);
	padding: 0 var(--sp-12);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-md);
	background: var(--bg-elevated);
	color: var(--text-primary);
	font: inherit;
	font-size: var(--fs-12);
	font-weight: 600;
	cursor: pointer;
	opacity: 0;
	transition: opacity var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.dm__row:hover .dm__rowJoin,
.dm__row:hover .dm__rowCall,
.dm__row:focus-within .dm__rowJoin,
.dm__row:focus-within .dm__rowCall,
.dm__rowJoin:focus-visible,
.dm__rowCall:focus-visible {
	opacity: 1;
}

.dm__rowJoin:hover,
.dm__rowCall:hover {
	background: var(--accent-soft);
	border-color: var(--accent);
	color: var(--accent);
}

@media (pointer: coarse) {
	/* Nothing may be reachable by hover only. */
	.dm__rowJoin,
	.dm__rowCall {
		opacity: 1;
	}
}

.dm__find .field__input {
	width: 100%;
}

/* --- empty state -------------------------------------------------------- */

.dm__empty {
	margin: var(--sp-48) auto;
	max-width: 380px;
	padding: 0 var(--sp-16);
	text-align: center;
	color: var(--text-secondary);
}

.dm__emptyTitle {
	margin: 0 0 var(--sp-6);
	color: var(--text-primary);
	font-size: var(--fs-16);
	font-weight: 600;
	line-height: var(--lh-heading);
}

.dm__feedback {
	margin: 0 0 var(--sp-4);
	font-size: var(--fs-12);
	color: var(--positive);
}

.dm__feedback--error {
	color: var(--critical);
}

/* =============================================================== thread pane */

.dm__threadPane {
	flex: 1;
	display: flex;
	flex-direction: column;
	min-width: 0;
	min-height: 0;
	background: var(--bg-canvas);
	position: relative;
}

.dm__threadHeader {
	display: flex;
	align-items: center;
	gap: var(--sp-12);
	min-height: 52px;
	padding: var(--sp-8) var(--sp-12);
	border-bottom: 1px solid var(--border-subtle);
	background: var(--bg-surface);
}

.dm__threadHeader .dm__avatar {
	width: var(--sp-32);
	height: var(--sp-32);
	font-size: var(--fs-12);
}

.dm__threadHeader .dm__avatar[data-online="yes"]::after {
	width: var(--sp-8);
	height: var(--sp-8);
	border-width: 2px;
	border-color: var(--bg-surface);
}

.dm__threadTitle {
	min-width: 0;
	flex: 1;
	display: flex;
	flex-direction: column;
}

.dm__threadTitle strong {
	font-size: var(--fs-14);
	font-weight: 600;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.dm__threadTitle small {
	color: var(--text-secondary);
	font-size: var(--fs-11);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.dm__join,
.dm__share {
	flex: none;
	margin-left: 0;
}

.dm__share + .dm__join {
	margin-left: var(--sp-8);
}

/* The back button is the mobile pop gesture made explicit; on desktop it stays
   available as a quiet shortcut back to the list. */
.dm__back {
	flex: none;
	width: var(--control-h);
	height: var(--control-h);
	padding: 0;
	border-radius: var(--radius-md);
	font-size: var(--fs-16);
	line-height: 1;
}

/* Roster of the open room's call, above the thread: who is in the room right
   now and what their media is doing, without joining the call. */
.dm__roster {
	flex: none;
	max-height: 34%;
	overflow-y: auto;
	padding: var(--sp-4) 0 var(--sp-8);
	border-bottom: 1px solid var(--border-subtle);
	background: var(--bg-surface);
}

.dm__roster[hidden] {
	display: none;
}

/* --- the message list --------------------------------------------------- */

.dm__thread {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	overscroll-behavior: contain;
	scroll-behavior: auto;
	padding: var(--dm-pane-pad);
	display: flex;
	flex-direction: column;
	gap: var(--sp-2);
}

/* Day divider: the thread is read as a diary, not as a wall of bubbles. */
.dm__day {
	align-self: stretch;
	display: flex;
	align-items: center;
	gap: var(--sp-12);
	margin: var(--sp-12) 0 var(--sp-8);
	color: var(--text-tertiary);
	font-size: var(--fs-11);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.04em;
}

.dm__day::before,
.dm__day::after {
	content: "";
	flex: 1;
	height: 1px;
	background: var(--border-subtle);
}

/* --- bubbles ------------------------------------------------------------ */

.bubble {
	position: relative;
	max-width: min(70ch, 78%);
	padding: var(--sp-8) var(--sp-12);
	border-radius: var(--radius-lg);
	background: var(--bg-elevated);
	color: var(--text-primary);
	font-size: var(--fs-14);
	line-height: var(--lh-prose);
	word-break: break-word;
	overflow-wrap: anywhere;
	/* The tail-side corner is tightened, which is what makes a bubble read as
	   "coming from" somebody rather than as a box. */
	border-bottom-left-radius: var(--radius-sm);
	transition: background var(--dur-fast) var(--ease);
}

/* First of a run gets air above it; the rest of the run stays tight. */
.bubble--first {
	margin-top: var(--sp-12);
}

.bubble--first:first-child {
	margin-top: 0;
}

/* Sent messages sit on the accent — the Teams chat look. */
.bubble--mine {
	align-self: flex-end;
	background: var(--accent);
	color: var(--text-on-accent);
	border-bottom-left-radius: var(--radius-lg);
	border-bottom-right-radius: var(--radius-sm);
}

.bubble--mine .bubble__meta,
.bubble--mine .bubble__who {
	color: color-mix(in srgb, var(--text-on-accent) 78%, transparent);
}

.bubble--mine .bubble__avatar {
	background: color-mix(in srgb, var(--text-on-accent) 22%, transparent);
	color: var(--text-on-accent);
}

/* Avatar and name only on the first message of a run (UI spec §4). */
.bubble__meta {
	display: flex;
	align-items: center;
	gap: var(--sp-8);
	margin-bottom: var(--sp-2);
	font-size: var(--fs-11);
	color: var(--text-secondary);
}

.bubble__meta[hidden] {
	display: none;
}

.bubble__avatar {
	flex: none;
	display: grid;
	place-items: center;
	width: var(--sp-24);
	height: var(--sp-24);
	border-radius: var(--radius-full);
	background: var(--accent-soft);
	color: var(--accent);
	font-size: var(--fs-11);
	font-weight: 600;
}

.bubble__who {
	display: flex;
	align-items: baseline;
	gap: var(--sp-8);
	min-width: 0;
}

.bubble__sender {
	font-weight: 600;
}

.bubble__time {
	color: inherit;
	opacity: 0.75;
	font-variant-numeric: tabular-nums;
}

.bubble__body {
	white-space: pre-wrap;
}

/* Hover/focus actions. Reachable by keyboard (focus-within) and always visible
   on touch — nothing important may live behind a hover. */
.bubble__actions {
	position: absolute;
	top: calc(-1 * var(--sp-8));
	right: var(--sp-8);
	display: flex;
	gap: var(--sp-2);
	padding: var(--sp-2);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-full);
	background: var(--bg-overlay);
	box-shadow: var(--shadow-2);
	opacity: 0;
	pointer-events: none;
	transition: opacity var(--dur-fast) var(--ease);
}

.bubble--mine .bubble__actions {
	right: auto;
	left: var(--sp-8);
}

.bubble:hover .bubble__actions,
.bubble:focus-within .bubble__actions {
	opacity: 1;
	pointer-events: auto;
}

.bubble__action {
	display: grid;
	place-items: center;
	width: var(--sp-24);
	height: var(--sp-24);
	border: 0;
	border-radius: var(--radius-full);
	background: transparent;
	color: var(--text-secondary);
	font: inherit;
	font-size: var(--fs-12);
	line-height: 1;
	cursor: pointer;
}

.bubble__action:hover {
	background: var(--bg-hover);
	color: var(--text-primary);
}

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

.bubble__delete:hover {
	background: var(--critical-soft);
	color: var(--critical);
}

.bubble__action.is-done {
	color: var(--positive);
}

@media (pointer: coarse) {
	.bubble__actions {
		position: static;
		justify-content: flex-end;
		margin-top: var(--sp-4);
		opacity: 1;
		pointer-events: auto;
		box-shadow: none;
		border-color: var(--border-subtle);
	}

	.bubble__action {
		width: 44px;
		height: 44px;
	}
}

/* Skeletons for a history that is on its way. Never a blank pane. */
.dm__skeletonBubble {
	max-width: 60%;
	padding: var(--sp-12);
	border-radius: var(--radius-lg);
	background: var(--bg-elevated);
	opacity: 0.6;
}

.dm__skeletonBubble:nth-child(3n) {
	align-self: flex-end;
}

.dm__skeletonBar {
	height: var(--sp-12);
	border-radius: var(--radius-sm);
	background: var(--bg-hover);
	animation: dm-skeleton 1.4s var(--ease) infinite;
}

@keyframes dm-skeleton {
	0%,
	100% {
		opacity: 1;
	}

	50% {
		opacity: 0.4;
	}
}

/* --- jump to latest -----------------------------------------------------
 *
 * The button floats over the bottom of the list, just above the typing line
 * and the composer. `--dm-chrome-h` is the height of that chrome (typing line
 * + composer); it is declared per surface because the in-call panel's
 * composer is smaller than the page's.
 */

.dm__threadPane,
.chat {
	--dm-chrome-h: 108px;
}

.dm__jump,
.chat__jump {
	position: absolute;
	bottom: var(--dm-chrome-h);
	left: 50%;
	transform: translateX(-50%);
	z-index: 3;
	padding: var(--sp-6) var(--sp-12);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-full);
	background: var(--bg-overlay);
	color: var(--text-primary);
	box-shadow: var(--shadow-2);
	font: inherit;
	font-size: var(--fs-12);
	font-weight: 600;
	cursor: pointer;
	animation: dm-rise var(--dur) var(--ease);
}

.chat {
	--dm-chrome-h: 92px;
}

@keyframes dm-rise {
	from {
		opacity: 0;
		transform: translate(-50%, var(--sp-6));
	}

	to {
		opacity: 1;
		transform: translate(-50%, 0);
	}
}

/* ============================================================== composer */

.chat__typing {
	min-height: var(--sp-16);
	padding: 0 var(--dm-pane-pad);
	color: var(--text-secondary);
	font-size: var(--fs-12);
}

.chat__typing:empty {
	min-height: 0;
	padding-bottom: 0;
}

.chat__composer {
	flex: none;
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	grid-template-areas: "files files" "tools send" "input input";
	column-gap: var(--sp-8);
	row-gap: var(--sp-8);
	padding: var(--sp-8) var(--dm-pane-pad) var(--sp-12);
	border-top: 1px solid var(--border-subtle);
	background: var(--bg-surface);
}

.chat__tools {
	grid-area: tools;
	display: flex;
	gap: var(--sp-4);
}

.chat__tool {
	display: grid;
	place-items: center;
	width: var(--control-h);
	height: var(--control-h);
	border: 0;
	border-radius: var(--radius-full);
	background: transparent;
	color: var(--text-secondary);
	font: inherit;
	font-size: var(--fs-16);
	line-height: 1;
	cursor: pointer;
}

.chat__tool:hover {
	background: var(--bg-hover);
	color: var(--text-primary);
}

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

.chat__files {
	grid-area: files;
	display: flex;
	flex-wrap: wrap;
	gap: var(--sp-8);
}

.chat__files:empty {
	display: none;
}

.chat__file {
	display: flex;
	align-items: center;
	gap: var(--sp-8);
	padding: var(--sp-4) var(--sp-8);
	border: 1px solid var(--border-subtle);
	border-radius: var(--radius-sm);
	background: var(--bg-elevated);
	font-size: var(--fs-11);
	color: var(--text-secondary);
}

.chat__file.is-done {
	color: var(--positive);
}

.chat__fileBar {
	display: block;
	width: 64px;
	height: var(--sp-4);
	border-radius: var(--radius-full);
	background: var(--bg-active);
	overflow: hidden;
}

.chat__fileFill {
	display: block;
	height: 100%;
	width: 0;
	border-radius: inherit;
	background: var(--accent);
	transition: width var(--dur) var(--ease);
}

.chat__input {
	grid-area: input;
	display: block;
	width: 100%;
	min-height: var(--control-h);
	max-height: var(--dm-composer-max);
	padding: var(--sp-8) var(--sp-12);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-md);
	background: var(--bg-elevated);
	color: var(--text-primary);
	font: inherit;
	font-size: var(--fs-14);
	line-height: var(--lh-prose);
	resize: none;
	overflow-y: hidden;
}

.chat__input::placeholder {
	color: var(--text-tertiary);
}

.chat__input:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: -1px;
	border-color: var(--accent);
}

.chat__send {
	grid-area: send;
	align-self: center;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--sp-8);
	min-width: var(--control-h);
	height: var(--control-h);
	padding: 0 var(--sp-12);
	border: 0;
	border-radius: var(--radius-full);
	background: var(--accent);
	color: var(--text-on-accent);
	font: inherit;
	font-size: var(--fs-12);
	font-weight: 600;
	cursor: pointer;
	transition: background var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}

.chat__send:hover:not(:disabled) {
	background: var(--accent-hover);
}

.chat__send:disabled {
	opacity: 0.5;
	cursor: default;
}

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

/* Sending is visible, not silent. */
.chat__composer.is-busy .chat__composerInput {
	opacity: 0.7;
}

/* Desktop shows the word, touch shows a big target with the icon only. */
.chat__sendIcon {
	font-size: var(--fs-12);
	line-height: 1;
}

@media (pointer: coarse) {
	.chat__send {
		width: 44px;
		height: 44px;
		padding: 0;
	}

	.chat__sendText {
		display: none;
	}
}

/* --- emoji picker ------------------------------------------------------- */

.chat__emojiPicker {
	position: absolute;
	z-index: 40;
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	gap: var(--sp-2);
	padding: var(--sp-6);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-lg);
	background: var(--bg-overlay);
	box-shadow: var(--shadow-3);
	animation: dm-rise var(--dur-fast) var(--ease);
}

.chat__emojiItem {
	width: var(--sp-32);
	height: var(--sp-32);
	border: 0;
	border-radius: var(--radius-md);
	background: transparent;
	font: inherit;
	font-size: var(--fs-16);
	line-height: 1;
	cursor: pointer;
}

.chat__emojiItem:hover {
	background: var(--bg-hover);
}

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

/* ============================================== in-call chat panel (overlay) */

.chat {
	position: relative;
	display: flex;
	flex-direction: column;
	min-height: 0;
	flex: 1;
	background: var(--bg-surface);
}

.chat__head {
	flex: none;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sp-8);
	padding: var(--sp-8) var(--sp-12);
	border-bottom: 1px solid var(--border-subtle);
}

.chat__title {
	margin: 0;
	font-size: var(--fs-13);
	font-weight: 600;
}

.chat__unread {
	display: grid;
	place-items: center;
	min-width: var(--sp-16);
	height: var(--sp-16);
	padding: 0 var(--sp-6);
	border-radius: var(--radius-full);
	background: var(--accent);
	color: var(--text-on-accent);
	font-size: var(--fs-11);
	font-weight: 700;
}

.chat__list {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	overscroll-behavior: contain;
	padding: var(--sp-12);
	display: flex;
	flex-direction: column;
	gap: var(--sp-2);
}

.chat__list .bubble {
	max-width: 88%;
	font-size: var(--fs-13);
}

/* ============================================================== row flash
 *
 * New activity briefly flashes the row it happened on. Transform/opacity only,
 * so it never costs a layout (spec §8), and it is a single class toggle from
 * `DmPage.flashRow`.
 */
.dm__row--flash {
	animation: dm-flash 900ms var(--ease);
}

@keyframes dm-flash {
	0% {
		background: var(--accent-soft);
	}

	100% {
		background: var(--bg-selected);
	}
}

@media (prefers-reduced-motion: reduce) {
	.dm__row--flash {
		animation: none;
		outline: 2px solid var(--accent);
		outline-offset: -2px;
	}

	.dm__skeletonBar,
	.chat__emojiPicker,
	.dm__jump,
	.chat__jump {
		animation: none;
	}
}

/* ================================================================= mobile
 *
 * The list and the thread are two screens, not two columns (spec §6): the
 * thread is pushed in from the right and the back button pops it. The panes
 * stay in the DOM, so their scroll positions survive the round trip.
 */
@media (max-width: 640px) {
	.dm__listPane {
		width: 100%;
		min-width: 0;
		border-right: 0;
	}

	#chatView .dm__threadPane,
	#chatView .dm__threadPane.is-open {
		position: absolute;
		inset: 0;
		z-index: 5;
		background: var(--bg-canvas);
		transform: translateX(100%);
		visibility: hidden;
		transition: transform var(--dur) var(--ease), visibility var(--dur) var(--ease);
	}

	#chatView .dm__threadPane.is-open {
		transform: translateX(0);
		visibility: visible;
	}

	.dm__threadHeader {
		/* The back button is the pop; it needs a finger-sized target. */
		padding-top: max(var(--sp-8), env(safe-area-inset-top));
		padding-bottom: var(--sp-8);
	}

	.dm__back {
		width: 44px;
		height: 44px;
		font-size: var(--fs-20);
	}

	.dm__row .dm__rowMain {
		grid-template-columns: 44px minmax(0, 1fr);
		padding: var(--sp-8);
	}

	.dm__avatar {
		width: 44px;
		height: 44px;
	}

	.dm__join,
	.dm__share {
		min-height: 44px;
	}

	.dm__thread {
		padding: var(--sp-12);
	}

	.bubble {
		max-width: 86%;
		font-size: var(--fs-16);
	}

	.chat__composer {
		padding-bottom: calc(var(--sp-12) + env(safe-area-inset-bottom));
	}

	.chat__input {
		font-size: var(--fs-16); /* 16 px keeps iOS from zooming on focus */
	}

	.chat__jump {
		top: auto;
		bottom: var(--sp-12);
	}
}

@media (min-width: 641px) {
	/* The id keeps this ahead of the shell's `#chatView .dm__threadPane`
	   fallback above, which is only there to undo the mobile overlay. */
	#chatView .dm__threadPane,
	#chatView .dm__threadPane.is-open {
		position: relative;
		inset: auto;
		transform: none;
		visibility: visible;
	}
}

/* ===================================================================== */
/* == VIDEO STAGE (spec §5) — owned by the video work                    */
/* ==                                                                   */
/* == Everything below replaces or completes the `.call__stage`, `.grid`,*/
/* == `.tile*`, `.statsOverlay*` and layout-menu rules further up. It is  */
/* == one banner-marked block on purpose: the stage is re-planned from    */
/* == measured numbers (see static/js/app/ui/videoLayout.js), and this is */
/* == the CSS that half of that job — the half a browser does better.     */
/* ===================================================================== */

/*
 * The stage is a size container, so tiles can react to the *stage* instead of
 * the window: a side panel docking, a phone rotating and a browser window
 * shrinking are all just a smaller stage, and one query covers all three.
 * `size` (not `inline-size`) is safe here because the stage is a `1fr` row of
 * a definite-height grid — it is never sized by its contents.
 */
.call__stage {
	container-type: size;
	container-name: stage;
}

/* ---------------------------------------------------------- the grid box */

.grid {
	position: relative;
	/* Rows come from the measurement as well as the columns, so the cells the
	   stylesheet draws are the cells the maths costed. */
	grid-template-rows: repeat(var(--rows, 1), minmax(0, 1fr));
	/* Chrome over a video is chrome, not a neighbour: the stage paints its own
	   letterbox colour, never the page background. */
	background: var(--stage-bg);
	/* Written by `VideoGrid._layout()` from the pure maths, so the stylesheet
	   and the legibility check cannot drift apart. */
	--tile-aspect: 16 / 9;
	--self-w: 180px;
}

/* The sidebar column is the measured one, not `22%` guessed twice: this is the
   width `sidebarGeometry()` costed, which is the width it proved legible for. */
.grid[data-layout="sidebar"] {
	grid-template-columns: minmax(0, 1fr) clamp(140px, var(--sidebar-w, 22%), 280px);
	grid-template-rows: minmax(0, 1fr);
}

/* Focus view: exactly one cell, whatever `--cols`/`--rows` said. */
.grid[data-layout="focus"] {
	grid-template-columns: minmax(0, 1fr);
	grid-template-rows: minmax(0, 1fr);
}

/* A pinned tile always takes the whole stage, whatever the layout is — the
   original rules placed it in every cell; this only has to undo the rows. */
.grid[data-layout="grid"] .tile--pinned {
	grid-column: 1 / -1;
	grid-row: 1 / -1;
}

.grid[data-layout="grid"] .tile--pinned~.tile {
	display: none;
}

/* Filmstrip: the measured width is a *cap*, so a short row of thumbnails is
   centred instead of stretching into a strip of letterbox. */
.grid[data-layout="speaker"] .tile:not(.tile--main):not(.tile--pinned) {
	height: var(--strip-h, clamp(70px, 16vh, 132px));
	max-width: var(--strip-w, none);
	justify-self: center;
}

.grid[data-layout="sidebar"] .tile:not(.tile--main):not(.tile--pinned) {
	justify-self: center;
	align-self: center;
}

/* ------------------------------------------------------------ fit / fill */

/*
 * "Fit" is the default and the honest one: the whole stream is visible and the
 * tile letterboxes around it over `--stage-bg`. "Fill" is offered as an
 * explicit presentation mode (`data-fit="fill"`) — it fills the tile and crops
 * the edges, which is prettier and loses whatever falls outside the tile.
 */
.grid[data-fit="fill"] {
	--tile-fit: cover;
}

.grid[data-fit="fill"] .tile {
	/* Nothing to letterbox against any more, so the tile chrome can go. */
	border-color: transparent;
}

/* -------------------------------------------------------------- the tiles */

.tile {
	box-shadow: var(--shadow-1);
	transition: box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

/* `contain` keeps a reacting video from repainting its neighbours, which is
   what a 12-tile grid spends its time doing. */
.tile__video {
	contain: paint;
}

/* The avatar placeholder of a camera-off tile, sized to the tile so it does
   not shout on a 400 px main tile and disappears into a 90 px thumbnail. */
.tile--audio-only::before {
	font-size: min(42px, 34cqh, 26cqw);
	color: var(--on-stage);
	opacity: 0.7;
}

/* Name + status row. Two declarations on purpose: the first one works
   everywhere (an opaque ramp built from the stage colour token), the second
   softens it wherever `color-mix()` exists. */
.tile__overlay {
	color: var(--on-stage);
	gap: var(--sp-6);
	padding: var(--sp-6) var(--sp-8);
	background: linear-gradient(to top, var(--stage-bg), transparent);
}

@supports (color: color-mix(in srgb, #000, #fff)) {
	.tile__overlay {
		background: linear-gradient(
			to top,
			color-mix(in srgb, var(--stage-bg) 74%, transparent),
			transparent 88%
		);
	}
}

.tile__name {
	font-weight: 600;
	/* A name must stay readable down to a 64 px tile: the row shrinks, it does
	   not disappear. */
	font-size: var(--fs-13);
	min-width: 0;
}

.tile__badges {
	gap: var(--sp-4);
	flex: none;
}

/* The quality dot only claims space when there is something to say. */
.tile__quality:empty {
	display: none;
}

.tile__quality::before {
	box-shadow: 0 0 0 2px color-mix(in srgb, var(--stage-bg) 60%, transparent);
}

/* Compact tiles: below the legible minimum the scrim gives way to a solid chip
   so the name keeps its contrast instead of fading into a 70 px video. The
   chips the tile shows (mic, camera, hand, pin, host) stay exactly where they
   are — compaction never removes information, it only re-skins it. */
.grid[data-compact="true"] .tile--compact .tile__overlay,
.tile--compact .tile__overlay {
	inset-inline: var(--sp-4);
	bottom: var(--sp-4);
	border-radius: var(--radius-sm);
	padding: 2px var(--sp-4);
	background: var(--stage-bg);
	font-size: var(--fs-11);
	opacity: 0.92;
}

.tile--compact .tile__name {
	font-size: var(--fs-11);
	font-weight: 500;
}

/* Per-tile statistics shrink with the tile instead of spilling over it. */
.tile--compact .tile__stats {
	font-size: var(--fs-11);
	padding: 2px var(--sp-4);
	top: var(--sp-4);
	right: var(--sp-4);
	inset-inline-start: auto;
	max-width: calc(100% - var(--sp-8));
	overflow: hidden;
}

/* ------------------------------------------------------- tile affordances */

/*
 * The pin/stats/fullscreen buttons sit at the *top* of the tile and the name at
 * the bottom, so revealing them can never cover the one thing that has to stay
 * readable. They are 32 px on a mouse and `--control-h` (44 px) on touch.
 */
.tile__actions {
	top: var(--sp-8);
	gap: var(--sp-4);
	transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.tile__action {
	width: var(--control-h);
	height: var(--control-h);
	border-radius: var(--radius-md);
	background: color-mix(in srgb, var(--stage-bg) 62%, transparent);
	border-color: color-mix(in srgb, var(--on-stage) 26%, transparent);
	color: var(--on-stage);
	font-size: var(--fs-13);
	transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

@supports not (backdrop-filter: blur(1px)) {
	.tile__action {
		background: var(--stage-bg);
	}
}

/* Touch has no hover: the buttons appear for the tile the user is on — the one
   they tapped, pinned, or reached with the keyboard — and stay there. */
@media (pointer: coarse) {
	.tile__actions {
		display: none;
	}

	.tile--pinned .tile__actions,
	.tile--main .tile__actions,
	.tile:focus-within .tile__actions,
	.tile:active .tile__actions {
		display: flex;
		opacity: 1;
		transform: none;
		pointer-events: auto;
	}
}

/* The active-speaker ring reads as accent, not green: the green bar is the
   loudness, the accent ring is "this is the one tile you are meant to look
   at", and colour is never the only signal — the ring is 2 px and persistent. */
.tile--speaking {
	box-shadow: 0 0 0 2px var(--accent);
}

.tile--connecting .tile__video {
	opacity: 0.75;
}

/* A whole-room quality problem is one badge on the stage, not N badges on
   tiles: the tiles already carry their own measured quality. */
.grid[data-quality="poor"] .tile:not(.tile--connecting):not(.tile--audio-only)::before,
.grid[data-quality="ok"] .tile:not(.tile--connecting):not(.tile--audio-only)::before {
	content: "";
	position: absolute;
	inset-block-start: 0;
	inset-inline: 0;
	height: 2px;
	background: var(--caution);
	pointer-events: none;
}

.grid[data-quality="poor"] .tile:not(.tile--connecting)::before {
	background: var(--critical);
}

/* ------------------------------------------------------------- self view */

/*
 * The local preview floats over the stage instead of taking a cell of its own,
 * so nobody's face loses 20% of the grid to it. It is small, it has a heavier
 * ring so it reads as chrome and not as a participant, and it docks in the free
 * cell of a partly filled grid — or, when the grid is full, in the corner the
 * stage can spare. It is still a real tile: it keeps its name, its mic state,
 * its reactions and its statistics.
 */
.tile--self-float {
	position: absolute;
	z-index: 4;
	width: var(--self-w, 180px);
	height: auto;
	aspect-ratio: var(--tile-aspect, 16 / 9);
	border-width: 2px;
	border-color: var(--border-strong);
	box-shadow: var(--shadow-3);
	touch-action: none;
	cursor: grab;
}

.tile--self-float.tile--dragging {
	cursor: grabbing;
	box-shadow: var(--shadow-3);
	opacity: 0.92;
}

.tile--self-float[data-selfpos="tl"] {
	inset-block-start: var(--tile-gap);
	inset-inline-start: var(--tile-gap);
}

.tile--self-float[data-selfpos="tr"] {
	inset-block-start: var(--tile-gap);
	inset-inline-end: var(--tile-gap);
}

.tile--self-float[data-selfpos="bl"] {
	inset-block-end: calc(var(--tile-gap) + var(--self-lift, 0px));
	inset-inline-start: var(--tile-gap);
}

.tile--self-float[data-selfpos="br"] {
	inset-block-end: calc(var(--tile-gap) + var(--self-lift, 0px));
	inset-inline-end: var(--tile-gap);
}

/* Being dragged (or nudged with the arrow keys): pixel coordinates instead of
   a corner anchor. */
.tile--self-float[data-selfpos="free"] {
	inset: auto;
	inset-inline-start: var(--self-x, 0px);
	inset-block-start: var(--self-y, 0px);
}

.tile--self-float .tile__actions {
	display: none;
}

.tile--self-float .tile__overlay {
	padding: 2px var(--sp-4);
	inset-inline: var(--sp-4);
	bottom: var(--sp-4);
	border-radius: var(--radius-sm);
	background: var(--stage-bg);
	font-size: var(--fs-11);
}

/* ---------------------------------------------------------- self a11y */

/* The polite live region: announced, never seen, never focusable. */
.grid__live {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* ------------------------------------------------------------- reactions */

.tile__reactions {
	position: absolute;
	inset-block-start: var(--sp-8);
	inset-inline-end: var(--sp-8);
	display: flex;
	flex-direction: column-reverse;
	align-items: flex-end;
	gap: var(--sp-4);
	pointer-events: none;
	z-index: 3;
}

/* Each reaction is its own element so a burst of them all plays: they rise out
   of the tile and fade, and they do so on top of the video — never in the
   place of the name. */
.tile__reaction {
	font-size: var(--fs-24);
	line-height: 1;
	filter: drop-shadow(0 2px 6px color-mix(in srgb, var(--stage-bg) 70%, transparent));
	animation: reaction-float var(--dur-slow) var(--ease) both;
	pointer-events: none;
}

.tile__reaction--gone {
	animation: reaction-fade var(--dur) var(--ease) both;
}

@keyframes reaction-float {
	from {
		opacity: 0;
		transform: translateY(12px) scale(0.6);
	}

	to {
		opacity: 1;
		transform: translateY(0) scale(1);
	}
}

@keyframes reaction-fade {
	to {
		opacity: 0;
		transform: translateY(-16px) scale(0.9);
	}
}

/* --------------------------------------------------------- container fit */

/*
 * Container queries on the stage. These are the cases a window media query
 * cannot express: the stage is the same window on a docked panel, and a phone
 * rotating is a different box entirely.
 */
@container stage (max-width: 560px) {
	.grid {
		gap: var(--sp-4);
		padding: var(--sp-4);
		--tile-gap: var(--sp-4);
	}

	.tile__name {
		font-size: var(--fs-12);
	}

	.tile__action {
		width: var(--control-h);
		height: var(--control-h);
	}
}

@container stage (max-width: 380px) {
	.grid {
		--tile-gap: var(--sp-2);
		gap: var(--sp-2);
		padding: var(--sp-2);
	}

	/* Below this there is no room for anything but the video and the name. */
	.tile__actions,
	.tile__stats {
		display: none;
	}
}

@container stage (max-height: 420px) {
	/* Height is the scarce resource in landscape on a phone: the chrome and
	   the controls thin out, the videos do not. */
	.grid[data-layout="speaker"] .tile:not(.tile--main):not(.tile--pinned) {
		height: var(--strip-h, 76px);
	}
}

/* ------------------------------------------------------- immersive stage */

/*
 * A real full-window stage. `#call` collapses its own rows when it is idle, so
 * the only thing left to add here is the part the browser does not do for us:
 * on a device without the Fullscreen API (every iPhone) the stage becomes the
 * whole visual viewport instead, which is the same thing the user asked for.
 * `dvh` tracks the URL bar as it collapses on both iOS Safari and Android
 * Chrome, so the videos never end up under the bar.
 */
.call.is-immersive .call__stage {
	/* `dvh` tracks the URL bar as it collapses on iOS Safari and Android
	   Chrome, so the stage is the *visible* window in both directions. */
	min-height: 100dvh;
	background: var(--stage-bg);
}

/*
 * The fallback half of immersive mode, and the only half iPhone Safari ever
 * gets: no Fullscreen API, so `#call` is promoted to a fixed, full-viewport
 * layer above the shell. Everything inside it keeps working — the chrome is
 * still there, it is just hidden until the user moves.
 */
.call.is-immersive {
	position: fixed;
	inset: 0;
	z-index: 60;
	height: 100dvh;
	background: var(--stage-bg);
}

/* The control bar floats over the whole window while immersive, so it has to
   clear the notch and the home indicator like it does in a normal call. */
.call.is-immersive .controls {
	padding-bottom: max(var(--sp-8), env(safe-area-inset-bottom));
}

/* ------------------------------------------------------ layout menu items */

/*
 * "Automatic" and the fit/fill switch are built into the layout menu from
 * `LAYOUTS` / `FIT_MODES` (see `App._buildLayoutMenu`) instead of being shipped
 * in the markup, so there is exactly one list of layouts in the codebase. They
 * reuse the popover item styling; only the auto item gets a leading rule so the
 * default reads as the first choice rather than as another presentation.
 */
#layoutMenu .popover__item[data-layout="auto"] {
	border-top: 1px solid var(--border-subtle);
	border-radius: 0 0 var(--radius-md) var(--radius-md);
	padding-top: var(--sp-6);
	margin-top: var(--sp-4);
}

#layoutMenu .popover__item[data-fit] {
	border-radius: var(--radius-md) var(--radius-md) 0 0;
}

/* The checkmark column of the radio list is the active item's accent bar; the
   fit switch uses it too, so "the videos are being cropped" is a visible state
   and not just a different sentence in a menu. */
#layoutMenu .popover__item.is-active[data-fit] {
	color: var(--text-primary);
}

/* ------------------------------------------------------- stats overlay */

/*
 * The statistics overlay docks over the stage (spec §5) and never takes a row
 * from it. On a phone it is a bottom sheet with a drag handle instead, so the
 * numbers never sit on top of the only tile the user cares about and the sheet
 * can be thrown away with a swipe.
 */
.statsOverlay {
	--sheet-radius: var(--radius-xl);
}

@media (max-width: 640px) {
	.statsOverlay {
		inset-inline: 0;
		inset-block-start: auto;
		inset-block-end: 0;
		width: auto;
		max-height: 62dvh;
		border-radius: var(--sheet-radius) var(--sheet-radius) 0 0;
		padding-bottom: max(var(--sp-8), env(safe-area-inset-bottom));
		box-shadow: var(--shadow-3);
		overscroll-behavior: contain;
	}

	/* The drag handle of the sheet. */
	.statsOverlay::before {
		content: "";
		display: block;
		width: 40px;
		height: 4px;
		margin: var(--sp-8) auto var(--sp-4);
		border-radius: var(--radius-full);
		background: var(--border-strong);
	}

	.statsOverlay__head {
		padding-top: 0;
	}
}

/* Landscape phone: the sheet can only take the bottom half, and the tiles have
   to stay visible above it. */
@media (max-height: 480px) and (orientation: landscape) {
	.statsOverlay {
		max-height: 88dvh;
	}
}

/* --------------------------------------------------------- reduced motion */

/* Motion in the stage is felt, not seen (spec §2): the reactions rise and the
   tiles settle, and both stop dead when the user asked for that. The global
   `prefers-reduced-motion` block above already neutralises animations; this one
   keeps the *state* changes — an entering reaction has to be visible as a
   state, not as an animation that got cut off. */
@media (prefers-reduced-motion: reduce) {
	.tile__reaction,
	.tile__reaction--gone {
		animation: none;
		opacity: 1;
	}

	.tile--self-float {
		transition: none;
	}
}
