/*
 * Components — ported from docs/mockup.html.
 *
 * Rules: variables only (see tokens.css). No hex colours, no font stacks, no
 * raw shadow values in this file.
 */

/* ---------- Screen-reader-only text ----------
 *
 * For labels that a screen reader needs and the eye does not: the name of a
 * select whose meaning is already obvious from the row it sits in, or the
 * colour behind a swatch. Four templates were already using this class before
 * it existed, which meant the text they meant to hide was simply on the page.
 *
 * Not `display: none` and not `visibility: hidden` — both remove the element
 * from the accessibility tree along with the layout, which is the opposite of
 * the point. The clip-rect dance is the one technique that leaves the text
 * readable to assistive technology while occupying no space. */

.sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* ---------- Icons ---------- */

.icon {
	flex-shrink: 0;
	stroke-width: 1.8;
}

/* ---------- Buttons ---------- */

.btn {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	padding: 6px 13px;
	border-radius: 8px;
	font-weight: 600;
	font-size: 12.5px;
	transition: background .12s, box-shadow .12s;
	white-space: nowrap;
	position: relative;
}

.btn .icon {
	width: 14px;
	height: 14px;
	stroke-width: 2;
}

.btn-primary {
	background: var(--primary);
	color: var(--on-primary);
	box-shadow: var(--shadow-sm);
}

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

.btn-ghost {
	background: var(--surface);
	border: 1px solid var(--border);
	color: var(--text-2);
}

.btn-ghost:hover {
	border-color: var(--border-strong);
	color: var(--text);
}

/* A ghost button that is the one you are already looking at — a row of scope
   tabs ("All / Mine / Unassigned") is three links, and without this the current
   one is indistinguishable from the two that would change the page. Soft rather
   than solid, because these sit beside a real primary action and must not
   compete with it. */
.btn-ghost.is-current {
	background: var(--primary-soft);
	border-color: var(--primary-soft-border);
	color: var(--primary);
}

.btn-ghost.is-current:hover {
	border-color: var(--primary-soft-border);
	color: var(--primary);
}

.btn-danger {
	background: var(--red);
	color: var(--on-primary);
}

.btn-danger:hover {
	background: var(--red);
	box-shadow: var(--shadow-md);
}

.btn:disabled,
.btn.is-loading {
	opacity: .65;
	cursor: not-allowed;
}

/* Spinner shown while the element's request is in flight (app.js adds .is-loading). */
.btn.is-loading::after {
	content: '';
	width: 12px;
	height: 12px;
	border-radius: 50%;
	border: 2px solid currentColor;
	border-right-color: transparent;
	animation: spin .6s linear infinite;
}

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

.icon-btn {
	width: 32px;
	height: 32px;
	border-radius: 8px;
	display: grid;
	place-items: center;
	color: var(--text-2);
	transition: background .12s;
	position: relative;
}

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

.icon-btn .icon {
	width: 17px;
	height: 17px;
	stroke-width: 1.8;
}

.icon-btn .dot {
	position: absolute;
	top: 7px;
	right: 7px;
	width: 7px;
	height: 7px;
	border-radius: 50%;
	background: var(--red);
	border: 2px solid var(--surface);
}

/* ---------- Badges & tags ---------- */

.badge {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	font-size: 11.5px;
	font-weight: 600;
	padding: 2px 9px;
	border-radius: 20px;
}

.badge::before {
	content: '';
	width: 5px;
	height: 5px;
	border-radius: 50%;
	background: currentColor;
}

.badge.info {
	color: var(--blue);
	background: var(--blue-soft);
}

.badge.success {
	color: var(--green);
	background: var(--green-soft);
}

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

.badge.danger {
	color: var(--red);
	background: var(--red-soft);
}

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

.badge.neutral {
	color: var(--text-3);
	background: var(--bg);
}

.tag {
	font-size: 10.5px;
	font-weight: 600;
	padding: 1px 7px;
	border-radius: 5px;
}

.tag.info {
	color: var(--blue);
	background: var(--blue-soft);
}

.tag.success {
	color: var(--green);
	background: var(--green-soft);
}

.tag.warning {
	color: var(--amber);
	background: var(--amber-soft);
}

.tag.danger {
	color: var(--red);
	background: var(--red-soft);
}

.tag.accent {
	color: var(--violet);
	background: var(--violet-soft);
}

.tag.neutral {
	color: var(--text-3);
	background: var(--bg);
}

/* ---------- Avatars ---------- */

.avatar {
	width: 28px;
	height: 28px;
	border-radius: 50%;
	display: grid;
	place-items: center;
	font-size: 11px;
	font-weight: 600;
	color: var(--on-primary);
	flex-shrink: 0;
}

.avatar.lg {
	width: 44px;
	height: 44px;
	font-size: 15px;
}

.avatar.sm {
	width: 22px;
	height: 22px;
	font-size: 9.5px;
}

.av-1 { background: var(--avatar-1); }
.av-2 { background: var(--avatar-2); }
.av-3 { background: var(--avatar-3); }
.av-4 { background: var(--avatar-4); }
.av-5 { background: var(--avatar-5); }
.av-6 { background: var(--avatar-6); }
.av-7 { background: var(--avatar-7); }
.av-8 { background: var(--avatar-8); }

/* ---------- Cards ---------- */

.card {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: var(--shadow-sm);
}

.card-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 14px 18px;
	border-bottom: 1px solid var(--border);
}

.card-title {
	font-size: 13.5px;
	font-weight: 600;
}

.card-link {
	font-size: 12px;
	font-weight: 600;
	color: var(--primary);
}

.card-body {
	padding: 16px 18px;
}

.card-foot {
	padding: 14px 18px;
	border-top: 1px solid var(--border);
	display: flex;
	justify-content: flex-end;
	gap: 8px;
}

/* ---------- KPI tiles ---------- */

.kpi-grid {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	gap: 14px;
	margin-bottom: 18px;
}

.kpi {
	padding: 16px 18px;
}

.kpi .label {
	font-size: 12px;
	font-weight: 500;
	color: var(--text-3);
	display: flex;
	align-items: center;
	justify-content: space-between;
}

.kpi .value {
	font-size: 24px;
	font-weight: 700;
	letter-spacing: -.5px;
	margin-top: 6px;
	font-variant-numeric: tabular-nums;
}

.kpi .delta {
	font-size: 11.5px;
	font-weight: 600;
	margin-top: 4px;
	display: inline-flex;
	align-items: center;
	gap: 4px;
	border-radius: 6px;
	padding: 1px 7px;
}

.delta.up {
	color: var(--green);
	background: var(--green-soft);
}

.delta.down {
	color: var(--red);
	background: var(--red-soft);
}

.delta .icon {
	width: 12px;
	height: 12px;
}

/* ---------- Tables ---------- */

.table-card {
	overflow: hidden;
}

table {
	width: 100%;
	border-collapse: collapse;
}

thead th {
	background: var(--surface-2);
	border-bottom: 1px solid var(--border);
	font-size: 11px;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .5px;
	color: var(--text-3);
	text-align: left;
	padding: 9px 16px;
	white-space: nowrap;
	user-select: none;
}

thead th.sortable {
	cursor: pointer;
}

thead th.sortable:hover {
	color: var(--text);
}

thead th .arr {
	font-size: 9px;
	margin-left: 4px;
}

thead th .icon {
	width: 12px;
	height: 12px;
	margin-left: 4px;
	vertical-align: -2px;
	opacity: .55;
}

tbody td {
	padding: 11px 16px;
	border-bottom: 1px solid var(--border);
	font-size: 13px;
	vertical-align: middle;
}

tbody tr {
	transition: background .1s;
}

tbody tr:hover {
	background: var(--surface-2);
}

tbody tr:last-child td {
	border-bottom: none;
}

.cell-person {
	display: flex;
	align-items: center;
	gap: 10px;
}

.cell-person .nm {
	font-weight: 600;
}

.cell-person .em {
	font-size: 11.5px;
	color: var(--text-3);
}

.mono {
	font-family: var(--mono);
	font-size: 12px;
	font-variant-numeric: tabular-nums;
}

.muted {
	color: var(--text-3);
}

/* ---------- Empty state ---------- */

.empty {
	padding: 48px 20px;
	text-align: center;
	color: var(--text-3);
}

.empty b {
	display: block;
	color: var(--text);
	font-size: 14px;
	margin-bottom: 4px;
}

.empty .icon {
	width: 22px;
	height: 22px;
	margin-bottom: 10px;
	color: var(--text-3);
}

/* ============================================================================
   THE CONTROL BASE LAYER
   ============================================================================

   Native controls, styled as elements. A classless <input>, <select> or
   <textarea> anywhere in this application comes out looking like the design
   system — which is the point: a template cannot forget a class it does not
   need.

   WHY THIS EXISTS. Before it, every control's appearance depended on a wrapper
   class, and three things had gone wrong at once:

     - `.search-input` was emitted by six templates and matched **no rule
       anywhere**. Six search boxes rendering as browser defaults because a
       class was invented and never written.
     - Filter-toolbar selects were emitted with no class at all, on the
       reasonable assumption that a base layer existed. It did not.
     - Even the wrapped controls kept the OS select arrow, because
       `appearance: none` appeared nowhere in the stylesheet.

   SCOPE. Element selectors, unscoped — components.css *is* the app scope. It
   is loaded by @layout.latte and @auth.latte and nothing else, so scoping
   these to `.app` would buy nothing and would drop the sign-in, register and
   reset screens, which are forms and need this as much as anything.

   SPECIFICITY, and the two traps either side of it. This layer has to weigh
   exactly (0,0,1) — one element selector. Not more, not less. The type
   exclusions therefore sit inside `:where()`, which contributes nothing, so
   `input:where(:not([type=checkbox], …))` scores the same as a bare `input`.

   Too heavy breaks the wrapper pattern. `.input input { border: none }` is how
   the search-box-with-an-icon works, and a plain
   `input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])`
   scores (0,4,1) — it would beat `.input input` (0,1,1) and put a second
   border inside every wrapper in the app.

   Too light loses to the reset. Writing the whole selector as `:where(input)`
   or `:where(select)` scores (0,0,0), which is *below* tokens.css's
   `input, select, textarea { font-size: inherit }` at (0,0,1) — so the
   selects and textareas silently kept the inherited 13.5px body size while
   the inputs took 13px, and a filter bar came out with its controls a
   fraction of a line apart. Measured, not guessed: that is why the `:where()`
   wraps the `:not()` and not the element.

   At (0,0,1) and loaded after tokens.css, this layer beats the reset and every
   class-based rule beats this layer. Which is what a base layer is. */

/* NO `width` here, deliberately. The base layer decides what a control *looks*
   like, never how much room it takes: `.filters` is a flex row, and a
   `width: 100%` base turns every filter into its own line — measured, the
   deals toolbar went from one row of five controls to five stacked rows.
   Filling the column is a property of being inside a `.field`, and that is
   where it is set. */
input:where(:not([type=checkbox], [type=radio], [type=range], [type=file], [type=hidden])),
select,
textarea {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 8px;
	padding: 7px 11px;
	font: inherit;
	font-size: 13px;
	line-height: 1.5;
	color: var(--text);
	transition: border-color .12s, box-shadow .12s;
}

/* ONE BASELINE. Every single-line control is the same height whatever its type
   or font-size, which is what lets a filter toolbar put a search box beside
   four selects and have them line up.

   35px = 19.5 (13px × 1.5 line-height) + 14 (7px padding, twice) + 2 (border),
   rounded down so a control whose natural height is a fraction taller is never
   squashed. It is a min-height rather than a height so a select with a long
   option or a browser with different metrics grows instead of clipping. */
input:where(:not([type=checkbox], [type=radio], [type=range], [type=file], [type=hidden])),
select {
	min-height: 35px;
}

/* The outline is suppressed only where the ring replaces it, so a control that
   somehow escapes this rule still gets the global :focus-visible outline from
   tokens.css rather than nothing at all. */
input:where(:not([type=checkbox], [type=radio], [type=range], [type=file], [type=hidden])):focus,
select:focus,
textarea:focus {
	border-color: var(--primary);
	outline: none;
	box-shadow: var(--focus-ring);
}

input:disabled,
select:disabled,
textarea:disabled {
	background: var(--surface-2);
	color: var(--text-3);
	cursor: not-allowed;
}

/* Readable, obviously not editable, and distinct from disabled — a readonly
   field is showing you a real value, a disabled one is switched off. */
input[readonly],
textarea[readonly] {
	background: var(--surface-2);
	color: var(--text-2);
}

/* ::placeholder is tokenized in tokens.css (--text-3) and inherited here. */

/* ---------- Selects: the OS arrow out, ours in ---------- */

select {
	appearance: none;
	-webkit-appearance: none;
	-moz-appearance: none;

	/* Room for the chevron, which is drawn in the right-hand padding. */
	padding-right: 30px;
	background-repeat: no-repeat;
	background-position: right 10px center;
	background-size: 14px 14px;

	/* THE ONE HARDCODED COLOUR IN THIS FILE, and it is a language limitation
	   rather than a choice: a CSS url() is an opaque string, so a data: URI
	   cannot read a custom property. #5C5C66 is --text-2 from tokens.css,
	   copied by hand — if that token ever changes, this changes with it.
	   (Same class of exception as the inline positioning styles noted under
	   AR-1 in docs/security-audit.md: the platform leaves no better option, so
	   it is written down instead of hidden.) */
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%235C5C66' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

/* A multiple or sized select is a list box, not a dropdown. It has no arrow to
   replace, and a chevron floating over its scrollable rows would be nonsense. */
select[multiple],
select[size]:not([size="1"]) {
	background-image: none;
	padding-right: 11px;
	height: auto;
}

/* ---------- Textareas ---------- */

textarea {
	min-height: 64px;
	resize: vertical;
	line-height: 1.5;
}

/* ---------- The odd-one-out input types ----------
   Date and time are the classic mismatch, and it is not the outer box that is
   wrong — it is the shadow tree inside it. Chromium builds a
   ::-webkit-datetime-edit containing a fields-wrapper containing one element
   per field, and several of those carry their own padding. Left alone the
   inner content computes 21.5px against a text input's 19.5px, so a
   "text / text / date" row comes out visibly stepped by exactly 2px.

   Measured in Chromium 1194: text 35.5px, date 37.5px before this block; all
   four types 35.5px after it. The individual field selectors are what actually
   does the work — zeroing only ::-webkit-datetime-edit leaves it at 37.5. */

input[type=date],
input[type=time],
input[type=datetime-local],
input[type=month],
input[type=week],
input[type=number] {
	line-height: 1.5;
}

input::-webkit-datetime-edit {
	line-height: 1.5;
	padding: 0;
}

input::-webkit-datetime-edit-fields-wrapper {
	padding: 0;
}

input::-webkit-datetime-edit-year-field,
input::-webkit-datetime-edit-month-field,
input::-webkit-datetime-edit-day-field,
input::-webkit-datetime-edit-hour-field,
input::-webkit-datetime-edit-minute-field,
input::-webkit-datetime-edit-second-field,
input::-webkit-datetime-edit-ampm-field,
input::-webkit-datetime-edit-text {
	padding: 0;
}

/* The calendar/clock button. Sized explicitly because its intrinsic box is
   another contributor to the height above. */
input::-webkit-calendar-picker-indicator {
	margin: 0;
	padding: 0;
	width: 14px;
	height: 14px;
	opacity: .6;
	cursor: pointer;
}

input::-webkit-calendar-picker-indicator:hover {
	opacity: 1;
}

/* ---------- Checkbox and radio ----------
   accent-color, not a custom-drawn control. It gives the native widget the
   brand colour while keeping the native focus ring, the native indeterminate
   state, forced-colors support and the OS's own hit targets — all of which a
   hand-drawn ::before has to reimplement and usually gets wrong. */

input[type=checkbox],
input[type=radio] {
	width: 16px;
	height: 16px;
	accent-color: var(--primary);
	cursor: pointer;
	flex: none;
}

input[type=checkbox]:disabled,
input[type=radio]:disabled {
	cursor: not-allowed;
	opacity: .55;
}

/* ---------- File input ----------
   The button half only. The surrounding "No file chosen" text is the browser's
   and cannot be styled, so the input itself stays borderless — a bordered box
   containing a button and grey system text looks like a mistake. */

input[type=file] {
	width: 100%;
	font: inherit;
	font-size: 13px;
	color: var(--text-2);
}

input[type=file]::file-selector-button {
	font: inherit;
	font-size: 12.5px;
	font-weight: 500;
	margin-right: 10px;
	padding: 6px 12px;
	border-radius: 8px;
	border: 1px solid var(--border);
	background: var(--surface);
	color: var(--text-2);
	cursor: pointer;
	transition: background .12s, border-color .12s;
}

input[type=file]::file-selector-button:hover {
	background: var(--surface-2);
	border-color: var(--border-strong);
}

/* ---------- Search inputs ----------
   WebKit decorates type=search with its own cancel button and inner padding,
   which makes an otherwise identical pair of inputs disagree by a few pixels
   and adds a second clear affordance next to any we draw ourselves. */

input[type=search] {
	-webkit-appearance: none;
	appearance: none;
}

input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-results-button,
input[type=search]::-webkit-search-results-decoration {
	-webkit-appearance: none;
	appearance: none;
}

/* ---------- fieldset / legend ----------
   Neutralized. The UA gives both a border, padding and a margin that no design
   in this application wants; grouping is done with .form-section. */

fieldset {
	border: 0;
	padding: 0;
	margin: 0;
	min-width: 0;
}

legend {
	padding: 0;
	font-size: 12px;
	font-weight: 600;
	color: var(--text-2);
}

/* ---------- Error state, at the base layer ----------
   `.has-error` works on the control itself or on any ancestor, so a template
   can mark whichever it has to hand. Class-based, so it outranks the base and
   the `.field`-scoped rules further down agree with it rather than fight. */

.has-error input,
.has-error select,
.has-error textarea,
input.has-error,
select.has-error,
textarea.has-error {
	border-color: var(--red);
}

.has-error input:focus,
.has-error select:focus,
.has-error textarea:focus,
input.has-error:focus,
select.has-error:focus,
textarea.has-error:focus {
	border-color: var(--red);
	box-shadow: var(--focus-ring-error);
}

/* ---------- Toolbar / filters / segmented control ---------- */

.toolbar {
	display: flex;
	align-items: center;
	gap: 10px;
	margin-bottom: 14px;
	flex-wrap: wrap;
}

.input {
	display: flex;
	align-items: center;
	gap: 8px;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 8px;
	padding: 6px 11px;
	min-width: 240px;
}

.input .icon {
	width: 15px;
	height: 15px;
	color: var(--text-3);
}

.input input {
	border: none;
	outline: none;
	background: none;
	width: 100%;
	font-size: 13px;
}

.seg {
	display: flex;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 8px;
	padding: 2px;
	gap: 2px;
}

.seg button {
	padding: 4px 12px;
	border-radius: 6px;
	font-size: 12.5px;
	font-weight: 500;
	color: var(--text-2);
}

.seg button.on {
	background: var(--primary-soft);
	color: var(--primary);
	font-weight: 600;
}

/* A filter-bar select. Everything structural — the border, the radius, the
   padding, the chevron, the height — now comes from the base layer; this is
   only the character the mockup gives a filter control, which is quieter and a
   little heavier than a form field.

   It deliberately no longer sets its own padding. It used to say 6px 10px,
   which after the base layer would have painted the chevron on top of the
   longest option's last letter, and would have made it a pixel shorter than
   the search box beside it. */
.select {
	font-size: 12.5px;
	color: var(--text-2);
	font-weight: 500;
}

/* The search box in a filter toolbar (six templates emit this class). Before
   this task it matched no rule at all — the class was invented and the CSS was
   never written, so every one of them rendered as a browser default. The base
   layer does the styling now; this is the one thing that is genuinely specific
   to it, which is that a search box should be wider than a filter dropdown and
   should not stretch to fill the bar. */
.search-input {
	width: auto;
	min-width: 220px;
	flex: 1 1 220px;
	max-width: 340px;
}

/* ---------- Form fields ---------- */

/* The styleguide's control-contract grid. Two columns so the four cards sit
   side by side and a control that has drifted is next to one that has not —
   which is how you notice. */
.sg-controls {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
	gap: 16px;
	align-items: start;
}

/* ---------- Vertical rhythm ----------
   Four knobs, and they are the only place form spacing is decided. If a form
   looks cramped, these move — not that form. A page-specific margin is how a
   design system stops being one. */

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

/* The border, radius, padding, height, focus ring and disabled treatment all
   come from the base layer now — a field's controls are ordinary controls.
   What is left here is the one thing that is true inside a .field and not
   outside it: the control fills the column it is given. */
/* `[type=color]` joins the exclusions for the same reason the other three are
   there: it is not a text box. Left in, it stretched to the column and a
   swatch picker read as a progress bar. */
.field input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=color]),
.field select,
.field textarea {
	width: 100%;
}

.field .hint {
	font-size: 11.5px;
	color: var(--text-3);
	margin-top: 4px;
}

.field .error {
	font-size: 11.5px;
	color: var(--red);
	font-weight: 500;
	margin-top: 4px;
	display: flex;
	align-items: center;
	gap: 4px;
}

.field .error .icon {
	width: 13px;
	height: 13px;
}

.field.has-error input,
.field.has-error select,
.field.has-error textarea {
	border-color: var(--red);
}

.field.has-error input:focus,
.field.has-error select:focus,
.field.has-error textarea:focus {
	box-shadow: var(--focus-ring-error);
}

.field-row {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 14px;
}

.form-body {
	padding: 18px 20px;
	display: grid;
	gap: 16px;
}

/* THE KNOB THAT REACHES EVERYTHING. Most real forms in this application are not
   in a `.modal-body` or a `.form-body` — they are in a plain `.card-body`,
   which had padding and nothing else. `display: block`, no gap: measured, the
   New deal form's five field rows sat at a visual gap of exactly **0px** and
   the only thing separating a control from the next label was the label's own
   line box. That is the cramped rhythm this task set out to fix, and it was
   never a modal problem.

   `:has()` rather than a new class on twenty templates, and rather than giving
   every `.card-body` a grid gap — a card body holding a table or a description
   list should keep the block layout it has always had. This says: a card body
   that contains form fields is a form body, and is spaced like one. Any form
   written after today gets it without knowing this rule exists. */
.card-body:has(> .field, > .field-row, > .form-section, > .custom-fields) {
	display: grid;
	gap: 16px;
	align-content: start;
}

/* A grouped block inside a form — the association pair on New deal, the
   custom-fields block on New deal and New lead. A bigger gap above it than the
   one between two ordinary fields is what makes the group read as a group,
   without needing a rule, a heading or a box drawn around it.

   22px above (16px of inherited grid gap + 6px here), 16px between its own
   fields. Enough to separate; not so much that the form falls into pieces. */
.form-section {
	display: grid;
	gap: 16px;
	margin-top: 6px;
}

/* An inline "add a note" box. The border, radius, resize behaviour and
   min-height are the base textarea's now; what makes it a note box rather than
   a form field is that it sits on the recessed surface and reads a touch
   smaller — it is a place to jot something, not a field you are filling in. */
.note-box {
	padding: 9px 11px;
	font-size: 12.5px;
	background: var(--surface-2);
}

/* A date that has already gone by on something still open (phase 23). Not a
   badge: the badge beside it says "Overdue" in words, and this is the date
   itself agreeing with it. */
.is-overdue {
	color: var(--red);
	font-weight: 600;
}

/* ---------- Callout ----------
   An inline panel that tells the reader something before they act — the
   company a conversion is about to reuse, a consequence a button has. Not a
   toast: a toast is about what just happened and goes away, this is about what
   is about to and stays on screen while they decide. */

.callout {
	display: flex;
	gap: 9px;
	align-items: flex-start;
	border: 1px solid var(--border);
	border-radius: 8px;
	padding: 10px 12px;
	font-size: 12.5px;
	line-height: 1.5;
	color: var(--text-2);
	background: var(--surface-2);
}

/* `display: flex` above beats the user agent's `[hidden] { display: none }` —
   author beats UA at equal specificity — so a callout given the `hidden`
   attribute by script stays on screen, empty, looking like a bug. It is: the
   softphone's problem callout shipped that way for exactly one screenshot.
   Fixed on the component rather than at each call site, because every feature
   that toggles a callout would otherwise discover it the same way. */
.callout[hidden] {
	display: none;
}

.callout .icon {
	flex: none;
	margin-top: 1px;
	color: var(--text-3);
}

.callout b {
	color: var(--text);
}

.callout.info {
	border-color: var(--primary-soft-border);
	background: var(--primary-soft);
}

.callout.info .icon {
	color: var(--primary);
}

/* The refusal variant, added with the documents module: an upload rejected by
   policy needs to look like a refusal, not like a note. */
.callout.danger {
	border-color: var(--red);
	background: var(--red-soft);
	color: var(--text);
}

.callout.danger .icon {
	color: var(--red);
}

/* ---------- Documents: file rows, drop zone, upload progress ---------- */

.file-list {
	display: flex;
	flex-direction: column;
}

.file-row {
	display: grid;
	grid-template-columns: auto 1fr auto;
	align-items: center;
	gap: 4px 10px;
	padding: 9px 0;
	border-bottom: 1px solid var(--border);
}

.file-row:last-child {
	border-bottom: 0;
}

.file-icon {
	display: inline-flex;
	color: var(--text-3);
}

.file-name {
	/* A long filename must not push the actions off the card, and truncating
	   in the middle of "Contratto-2026-final-v3.pdf" is the same as not
	   showing it — so it ellipsises and the full name is the title. */
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-weight: 600;
	color: var(--text);
}

.file-meta {
	grid-column: 2;
	font-size: 11.5px;
}

.file-actions {
	grid-row: 1 / span 2;
	grid-column: 3;
	display: flex;
	align-items: center;
	gap: 2px;
}

.file-actions form {
	display: contents;
}

/* ---- the drop zone ---- */

.file-drop {
	position: relative;
	margin-top: 12px;
	border: 1px dashed var(--border);
	border-radius: 8px;
	background: var(--surface-2);
	transition: border-color .12s, background .12s;
}

.file-drop.is-dragging {
	border-color: var(--primary);
	background: var(--primary-soft);
}

.file-drop-label {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 12px;
	cursor: pointer;
	color: var(--text-2);
}

.file-drop-label .icon {
	flex: none;
	color: var(--text-3);
}

.file-drop-text {
	display: flex;
	flex-direction: column;
	gap: 1px;
	font-size: 12.5px;
	line-height: 1.4;
}

.file-drop-text b {
	color: var(--text);
}

.file-drop-text .mono {
	font-size: 11px;
}

/* Visually hidden, not display:none — a file input that is not rendered
   cannot be opened by clicking its <label>, and the label is the whole
   affordance. */
.file-drop-input {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

.file-drop-input:focus-visible + .file-drop-label,
.file-drop-label:focus-within {
	outline: 2px solid var(--primary);
	outline-offset: 2px;
}

/* ---- the progress bar ---- */

.upload-progress {
	position: relative;
	display: flex;
	align-items: center;
	gap: 8px;
	height: 6px;
	margin: 0 12px 12px;
	border-radius: 999px;
	background: var(--border);
}

.upload-progress-bar {
	height: 100%;
	border-radius: 999px;
	background: var(--primary);
	/* Short, because the transition is between real measurements — a long one
	   would make the bar lag behind the upload and finish after it. */
	transition: width .15s linear;
}

.upload-progress-label {
	position: absolute;
	right: 0;
	top: 10px;
	font-size: 10.5px;
	color: var(--text-3);
}

/* ---------- Tabs ---------- */

.tabs {
	display: flex;
	gap: 2px;
	padding: 0 14px;
	border-bottom: 1px solid var(--border);
}

.tabs button {
	padding: 10px 12px;
	font-size: 12.5px;
	font-weight: 600;
	color: var(--text-3);
	border-bottom: 2px solid transparent;
	margin-bottom: -1px;
}

.tabs button.on {
	color: var(--primary);
	border-color: var(--primary);
}

/* ---------- Dropdown menu ---------- */

.dropdown {
	position: relative;
	display: inline-block;
}

.dropdown-menu {
	position: absolute;
	top: calc(100% + 6px);
	right: 0;
	min-width: 180px;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 10px;
	box-shadow: var(--shadow-md);
	padding: 5px;
	z-index: 30;
	display: none;
}

.dropdown.open .dropdown-menu {
	display: block;
}

.dropdown-item {
	display: flex;
	align-items: center;
	gap: 9px;
	width: 100%;
	padding: 7px 9px;
	border-radius: 7px;
	font-size: 12.5px;
	font-weight: 500;
	color: var(--text-2);
	text-align: left;
}

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

.dropdown-item.danger {
	color: var(--red);
}

.dropdown-item .icon {
	width: 15px;
	height: 15px;
}

.dropdown-sep {
	height: 1px;
	background: var(--border);
	margin: 5px 0;
}

/* ---------- Overlay, drawer, modal ---------- */

.overlay {
	position: fixed;
	inset: 0;
	background: var(--overlay);
	opacity: 0;
	pointer-events: none;
	transition: opacity .18s;
	z-index: 40;
}

.overlay.show {
	opacity: 1;
	pointer-events: auto;
}

.drawer {
	position: fixed;
	top: 0;
	right: -420px;
	width: 420px;
	max-width: 92vw;
	height: 100vh;
	background: var(--surface);
	border-left: 1px solid var(--border);
	box-shadow: var(--shadow-lg);
	z-index: 50;
	transition: right .22s cubic-bezier(.4, 0, .2, 1);
	display: flex;
	flex-direction: column;
}

.drawer.open {
	right: 0;
}

.drawer-head {
	padding: 18px 20px;
	border-bottom: 1px solid var(--border);
	display: flex;
	align-items: flex-start;
	gap: 13px;
}

.drawer-head .nm {
	font-size: 16px;
	font-weight: 700;
	letter-spacing: -.2px;
}

.drawer-head .rl {
	font-size: 12.5px;
	color: var(--text-3);
}

.drawer-close {
	margin-left: auto;
}

.drawer-body {
	flex: 1;
	overflow-y: auto;
	padding: 18px 20px;
}

.drawer-foot {
	padding: 14px 20px;
	border-top: 1px solid var(--border);
	display: flex;
	gap: 8px;
}

.dl {
	display: grid;
	grid-template-columns: 110px 1fr;
	row-gap: 12px;
	font-size: 12.5px;
}

.dl dt {
	color: var(--text-3);
	font-weight: 500;
}

.dl dd {
	font-weight: 500;
}

/*
 * `place-items: safe center` and `overflow: auto`, and both are load-bearing on
 * a phone.
 *
 * A grid item centred with plain `center` and taller than its container
 * overflows EQUALLY at both ends — so the top of a tall modal ends up above the
 * viewport, where no amount of scrolling reaches it, and the dialog's title and
 * close button are simply gone. `safe` says: centre it while it fits, and align
 * it to the start the moment it does not. With `overflow: auto` the rest of it
 * is then scrollable.
 *
 * Suspected, and then cleared, of being the source of a 62px document overflow
 * on the lead-sources screen at 390px: removing this element from the DOM left
 * documentElement.scrollHeight at 906 either way, and its card measures 252px
 * inside an 844px box. It is not the cause of that, and the note is left here
 * because the next person will suspect it too. The alignment fix above stands
 * on its own — a modal taller than the viewport is unreadable at the top, which
 * is a real failure on a short phone whatever else is true.
 */
.modal {
	position: fixed;
	inset: 0;
	display: grid;
	place-items: safe center;
	overflow: auto;
	overscroll-behavior: contain;
	padding: 16px;
	z-index: 60;
	opacity: 0;
	pointer-events: none;
	transition: opacity .16s;
}

.modal.show {
	opacity: 1;
	pointer-events: auto;
}

.modal-card {
	width: 460px;
	max-width: 92vw;
	background: var(--surface);
	border-radius: 14px;
	box-shadow: var(--shadow-lg);
	transform: translateY(8px) scale(.98);
	transition: transform .16s;

	/* A MODAL NEVER PAINTS OUTSIDE ITSELF.
	 *
	 * Reported from a live server: rows in the "Add a widget" picker were drawn
	 * past the right edge of the card, on top of the backdrop. The cause was in
	 * the picker (a grid column that could not shrink below its widest row's
	 * content — see `.picker-list`), but the card let it happen: with
	 * `overflow: visible` a body that is too wide is simply drawn where it
	 * lands, and the rounded corner it escapes is a strong visual signal that
	 * something is broken.
	 *
	 * This is the belt to that fix's braces, and it is deliberately on the
	 * SHELL rather than on the one body that misbehaved: the next modal body
	 * will be written by somebody who has not read this comment, and a dialog
	 * that clips is always better than one that bleeds. `hidden` rather than
	 * `auto` because the card must not grow its own scrollbar — the BODY
	 * scrolls (below), so the head and the footer buttons stay put. */
	overflow: hidden;
}

.modal.show .modal-card {
	transform: none;
}

.modal-head {
	padding: 16px 20px;
	border-bottom: 1px solid var(--border);
	display: flex;
	align-items: center;
}

.modal-head .t {
	font-size: 14.5px;
	font-weight: 700;
}

.modal-body {
	padding: 18px 20px;
	display: grid;
	gap: 16px;

	/* THE LIST SCROLLS INSIDE THE MODAL, rather than the modal growing past the
	 * screen. `.modal` itself scrolls too (it is a centred grid over the whole
	 * viewport), which handles a tall body — but scrolling the backdrop moves
	 * the title and the Cancel button off-screen with it. A picker with twenty
	 * widgets should scroll its list and keep its chrome.
	 *
	 * `min-height: 0` because this is a grid ITEM of the card: without it a
	 * grid item refuses to shrink below its content, and `max-height` on a box
	 * that will not shrink does nothing at all. */
	min-height: 0;
	max-height: min(60vh, 560px);
	overflow-y: auto;
	overscroll-behavior: contain;
}

.modal-foot {
	padding: 14px 20px;
	border-top: 1px solid var(--border);
	display: flex;
	justify-content: flex-end;
	gap: 8px;
}

/* The foot's twin for a modal whose BODY is swapped between steps.
 *
 * `.modal-foot` sits outside the body, so a multi-step dialog whose buttons
 * lived there would need each fragment to ship an `hx-swap-oob` copy of the
 * whole foot — and an oob element replaces its original, so it works exactly
 * once, the first time somebody forgets an attribute on it. A step that owns
 * its own actions has no twin to keep in step.
 *
 * No `border-top`: it is inside the scrolling body, and a rule that scrolls
 * reads as a divider between two paragraphs rather than as the edge of the
 * dialog. */
.modal-actions {
	display: flex;
	justify-content: flex-end;
	gap: 8px;
	flex-wrap: wrap;
}

/* ---------- Command palette ---------- */

.cmdk {
	position: fixed;
	inset: 0;
	z-index: 70;
	display: none;
	background: var(--overlay);
}

.cmdk.show {
	display: block;
}

.cmdk-card {
	width: 560px;
	max-width: 92vw;
	margin: 12vh auto 0;
	background: var(--surface);
	border-radius: 14px;
	box-shadow: var(--shadow-lg);
	overflow: hidden;
	border: 1px solid var(--border);
}

.cmdk-input {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 14px 16px;
	border-bottom: 1px solid var(--border);
}

.cmdk-input .icon {
	width: 16px;
	height: 16px;
	color: var(--text-3);
	stroke-width: 2;
}

.cmdk-input input {
	border: none;
	outline: none;
	width: 100%;
	font-size: 14px;
	background: none;
}

.cmdk-list {
	max-height: 320px;
	overflow-y: auto;
	padding: 6px;
}

.cmdk-item {
	display: flex;
	align-items: center;
	gap: 11px;
	padding: 9px 11px;
	border-radius: 8px;
	cursor: pointer;
	font-size: 13px;
	width: 100%;
	text-align: left;
}

.cmdk-item .k {
	font-size: 10.5px;
	color: var(--text-3);
	text-transform: uppercase;
	letter-spacing: .5px;
	font-weight: 600;
	margin-left: auto;
}

.cmdk-item .icon {
	width: 15px;
	height: 15px;
	color: var(--text-3);
}

.cmdk-item:hover,
.cmdk-item.sel {
	background: var(--primary-soft);
}

.cmdk-empty {
	padding: 24px;
	text-align: center;
	color: var(--text-3);
	font-size: 13px;
}

/* ---------- Off-canvas drawer ----------
 *
 * Shell chrome, like the palette above it: one box, filled by whichever module
 * aimed a request at #offcanvas-panel. Between the palette (70) and the toasts
 * (80) on purpose — a drawer covers the page, and a toast covers the drawer,
 * because "Sent." is the answer to what somebody just did in it.
 */

.offcanvas {
	position: fixed;
	inset: 0;
	z-index: 75;
}

.offcanvas[hidden] {
	display: none;
}

.offcanvas-backdrop {
	position: absolute;
	inset: 0;
	background: var(--overlay);
	opacity: 0;
	transition: opacity .18s ease;
}

.offcanvas.is-open .offcanvas-backdrop {
	opacity: 1;
}

.offcanvas-panel {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	width: min(680px, 100vw);
	background: var(--surface);
	border-left: 1px solid var(--border);
	box-shadow: var(--shadow-lg);
	display: flex;
	flex-direction: column;
	overflow: hidden;
	/* Off-screen until open, so the first frame is a slide rather than a jump. */
	transform: translateX(100%);
	transition: transform .18s ease;
}

/*
 * Flex children default to `min-width: auto`, which means a long word or a
 * wide control makes the column wider than its parent instead of wrapping.
 * The composer is a column of full-width fields inside two nested flex boxes,
 * so both of them have to be told.
 */
.offcanvas-panel > *,
.offcanvas-panel form {
	min-width: 0;
}

.offcanvas.is-open .offcanvas-panel {
	transform: translateX(0);
}

.offcanvas-panel:focus {
	outline: none;
}

/*
 * The page behind does not scroll while a panel is over it — and the class goes
 * on the ROOT element, not on <body>. The shell's own comment says why: the
 * viewport's scrollbar is governed by <html> here, and `overflow: hidden` on
 * <body> alone leaves the document scrollable.
 */
html.offcanvas-open {
	overflow: hidden;
}

@media (max-width: 820px) {
	/*
	 * `left: 0` rather than `width: 100vw`: 100vw INCLUDES the scrollbar's
	 * width, so on a page that has one the panel is a few pixels wider than the
	 * space it has and its right-hand edge — the Load button, the ends of the
	 * fields — is cut off. Pinning both edges cannot be wrong by a scrollbar.
	 */
	.offcanvas-panel {
		left: 0;
		width: auto;
		border-left: none;
	}
}

/* ---------- Toasts ---------- */

.toasts {
	position: fixed;
	bottom: 20px;
	right: 20px;
	z-index: 80;
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.toast {
	background: var(--text);
	color: var(--on-primary);
	font-size: 12.5px;
	font-weight: 500;
	padding: 10px 15px;
	border-radius: 10px;
	box-shadow: var(--shadow-lg);
	display: flex;
	align-items: center;
	gap: 9px;
	animation: toastIn .2s ease;
	transition: opacity .25s;
}

.toast .icon {
	width: 14px;
	height: 14px;
	stroke-width: 2.4;
}

.toast.success .icon {
	color: var(--toast-icon);
}

.toast.info .icon {
	color: var(--blue-soft);
}

.toast.warning .icon {
	color: var(--amber-soft);
}

.toast.error .icon {
	color: var(--red-soft);
}

.toast.leaving {
	opacity: 0;
}

@keyframes toastIn {
	from {
		opacity: 0;
		transform: translateY(8px);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* ---------- Global progress bar ---------- */

#progress {
	position: fixed;
	top: 0;
	left: 0;
	height: 2px;
	width: 0;
	background: var(--primary);
	box-shadow: var(--shadow-sm);
	z-index: 90;
	opacity: 0;
	transition: width .2s ease, opacity .3s ease;
	pointer-events: none;
}

#progress.active {
	opacity: 1;
}

/* ---------- Skeletons ---------- */

.skeleton {
	background: var(--skeleton);
	border-radius: 6px;
	position: relative;
	overflow: hidden;
}

.skeleton::after {
	content: '';
	position: absolute;
	inset: 0;
	transform: translateX(-100%);
	background: linear-gradient(90deg, transparent, var(--skeleton-shine), transparent);
	animation: shimmer 1.2s infinite;
}

@keyframes shimmer {
	100% {
		transform: translateX(100%);
	}
}

.skeleton-line {
	height: 10px;
	margin: 7px 0;
}

.skeleton-row {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 11px 16px;
	border-bottom: 1px solid var(--border);
}

.skeleton-row:last-child {
	border-bottom: none;
}

.skeleton-avatar {
	width: 28px;
	height: 28px;
	border-radius: 50%;
	flex-shrink: 0;
}

.skeleton-card {
	padding: 16px 18px;
}

/* HTMX indicators: hidden until the owning request is in flight. */
.htmx-indicator {
	display: none;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
	display: block;
}

/* ---------- Permission matrix ---------- */

.matrix-wrap {
	overflow: auto;
	max-height: calc(100vh - 210px);
}

table.matrix {
	border-collapse: separate;
	border-spacing: 0;
}

table.matrix th,
table.matrix td {
	border-bottom: 1px solid var(--border);
}

table.matrix thead th {
	position: sticky;
	top: 0;
	z-index: 3;
	background: var(--surface-2);
}

.matrix-head-cell {
	position: sticky;
	left: 0;
	z-index: 2;
	background: var(--surface);
	text-align: left;
	min-width: 260px;
}

table.matrix thead .matrix-head-cell {
	z-index: 4;
	background: var(--surface-2);
}

.matrix-role {
	padding: 8px 14px;
	min-width: 132px;
	text-align: center;
	vertical-align: bottom;
}

.matrix-role .nm {
	display: block;
	font-size: 12px;
	font-weight: 600;
	color: var(--text);
	text-transform: none;
	letter-spacing: 0;
}

.matrix-role .mono {
	display: block;
	font-size: 10.5px;
	color: var(--text-3);
	text-transform: none;
	letter-spacing: 0;
}

.matrix-role.is-locked .nm {
	color: var(--primary);
}

.matrix-role .hint {
	display: block;
	font-size: 9.5px;
	font-weight: 500;
	color: var(--text-3);
	text-transform: none;
	letter-spacing: 0;
	margin-top: 2px;
}

.matrix-group th {
	background: var(--surface-2);
	padding: 0;
}

.group-toggle {
	display: flex;
	align-items: center;
	gap: 8px;
	width: 100%;
	padding: 8px 16px;
	font-size: 11px;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .5px;
	color: var(--text-2);
}

.group-toggle .icon {
	transition: transform .15s;
}

.group-toggle[aria-expanded="false"] .icon {
	transform: rotate(-90deg);
}

.group-toggle .count {
	font-size: 10.5px;
	font-weight: 600;
	color: var(--text-3);
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 6px;
	padding: 0 6px;
	line-height: 17px;
}

.matrix-perm {
	padding: 9px 16px;
	font-weight: 400;
	display: table-cell;
	position: relative;
}

.matrix-perm .nm {
	font-size: 12.5px;
	font-weight: 600;
	color: var(--text);
	text-transform: none;
	letter-spacing: 0;
}

.matrix-perm .mono {
	font-size: 11px;
	text-transform: none;
	letter-spacing: 0;
}

.matrix-perm form {
	position: absolute;
	top: 6px;
	right: 8px;
	opacity: 0;
	transition: opacity .12s;
}

.matrix-perm:hover form {
	opacity: 1;
}

.matrix-cell {
	text-align: center;
	padding: 6px;
}

.cell-toggle {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 6px;
	min-height: 22px;
	cursor: pointer;
}

.cell-toggle input[type="checkbox"] {
	width: 16px;
	height: 16px;
	accent-color: var(--primary);
	cursor: pointer;
}

.matrix-cell.is-locked .cell-toggle,
.matrix-cell.is-locked input {
	cursor: not-allowed;
	opacity: .55;
}

.cell-spinner {
	color: var(--primary);
	line-height: 0;
}

.cell-spinner .icon {
	animation: spin .8s linear infinite;
}

.htmx-request .cell-spinner.htmx-indicator,
.htmx-request.cell-spinner.htmx-indicator {
	display: inline-block;
}

/* ---------- Admin lists ---------- */

.inline-rename {
	display: flex;
	align-items: center;
	gap: 4px;
}

.rename-input {
	border: 1px solid transparent;
	border-radius: 7px;
	padding: 4px 8px;
	font-size: 13px;
	font-weight: 600;
	background: transparent;
	min-width: 160px;
}

.rename-input:hover {
	border-color: var(--border);
}

.rename-input:focus {
	border-color: var(--primary);
	outline: none;
	box-shadow: var(--focus-ring);
	background: var(--surface);
}

.result-list {
	display: flex;
	flex-direction: column;
	max-height: 340px;
	overflow-y: auto;
}

.result-row {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 9px 18px;
	border-top: 1px solid var(--border);
	text-align: left;
	width: 100%;
}

.result-row:hover {
	background: var(--surface-2);
}

.result-row .meta {
	min-width: 0;
	line-height: 1.3;
}

.result-row .n {
	display: block;
	font-size: 12.5px;
	font-weight: 600;
}

.result-row .e {
	display: block;
	font-size: 11.5px;
	color: var(--text-3);
}

.role-list {
	display: flex;
	flex-direction: column;
}

.role-row {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 11px 18px;
	border-top: 1px solid var(--border);
	flex-wrap: wrap;
}

.role-row .n {
	font-weight: 600;
}

.role-row .mono {
	margin-left: 6px;
	font-size: 11px;
}

.role-row.has-error {
	background: var(--red-soft);
}

.role-row .error {
	flex-basis: 100%;
	display: flex;
	align-items: center;
	gap: 5px;
	font-size: 11.5px;
	font-weight: 500;
	color: var(--red);
}

/* ---------- Kanban board (ported from the mockup) ---------- */

.kanban {
	display: grid;
	grid-auto-flow: column;
	grid-auto-columns: 272px;
	gap: 14px;
	overflow-x: auto;
	padding-bottom: 14px;
	align-items: start;
}

.kcol {
	background: var(--surface-2);
	border: 1px solid var(--border);
	border-radius: 12px;
	display: flex;
	flex-direction: column;
	max-height: calc(100vh - 220px);
}

.kcol-head {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 11px 13px;
	border-bottom: 1px solid var(--border);
}

.kcol-head .dot {
	width: 8px;
	height: 8px;
	border-radius: 50%;
}

.kcol-head .nm {
	font-size: 12.5px;
	font-weight: 600;
}

.kcol-head .cnt {
	font-size: 11px;
	font-weight: 600;
	color: var(--text-3);
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 6px;
	padding: 0 6px;
	line-height: 17px;
	margin-left: auto;
}

.kcol-body {
	padding: 10px;
	display: flex;
	flex-direction: column;
	gap: 8px;
	overflow-y: auto;
	min-height: 60px;
}

.kcol-body.dragover {
	background: var(--primary-soft);
	border-radius: 0 0 12px 12px;
}

.kcol-empty {
	padding: 18px 8px;
	text-align: center;
	font-size: 12px;
	color: var(--text-3);
}

.deal {
	position: relative;
	display: block;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 10px;
	padding: 11px 12px;
	box-shadow: var(--shadow-sm);
	color: var(--text);
	text-decoration: none;
	transition: box-shadow .12s, border-color .12s;
}

.deal[draggable="true"] {
	cursor: grab;
}

.deal:hover {
	border-color: var(--border-strong);
	box-shadow: var(--shadow-md);
}

.deal.dragging {
	opacity: .45;
	cursor: grabbing;
}

.deal .co {
	font-size: 11px;
	font-weight: 600;
	color: var(--text-3);
	text-transform: uppercase;
	letter-spacing: .4px;
}

/* The column's money total, beside its count (phase 23). Compact in the header
   with the exact figure on hover — a header has no room for eleven digits, and
   the point of a column total is the order of magnitude. */
.kcol-sum {
	font-size: 11px;
	font-weight: 600;
	color: var(--text-2);
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 6px;
	padding: 0 6px;
	line-height: 17px;
}

/* With a sum present the count no longer needs to push itself right. */
.kcol-head .cnt + .kcol-sum {
	margin-left: 0;
}

/* A deal card's amount, between the name and the footer. */
.deal .val {
	font-size: 12.5px;
	font-weight: 600;
	color: var(--text);
	margin: -4px 0 8px;
}

.deal .nm {
	display: block;
	font-size: 13px;
	font-weight: 600;
	margin: 3px 0 8px;
	line-height: 1.35;
	color: var(--text);
	text-decoration: none;
}

/* The name is the link, stretched over the whole card — so the card clicks
   like a link while the card itself stays a plain draggable element. */
.deal .nm::after {
	content: '';
	position: absolute;
	inset: 0;
	border-radius: 10px;
}

.deal:hover .nm {
	color: var(--primary);
}

/* Anything the user should be able to reach sits above the stretched link. */
.deal .foot {
	position: relative;
	z-index: 1;
}

.deal .foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
}

.deal .due {
	font-size: 11px;
	color: var(--text-3);
	display: flex;
	align-items: center;
	gap: 4px;
}

/* ---------- Leads: stages, detail, widgets ---------- */

/* Stage colours are palette names, never raw hex — the picker offers these
   seven and the badge, the dot and the bar all read from the same list.
   `gray` is the neutral, added for ticket statuses: a closed request is not a
   lost one, and making the only muted option red said the wrong thing. */
.stage-badge {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	font-size: 11.5px;
	font-weight: 600;
	padding: 2px 9px;
	border-radius: 20px;
}

.stage-badge::before {
	content: '';
	width: 5px;
	height: 5px;
	border-radius: 50%;
	background: currentColor;
}

.stage-blue    { color: var(--blue);    background: var(--blue-soft); }
.stage-violet  { color: var(--violet);  background: var(--violet-soft); }
.stage-amber   { color: var(--amber);   background: var(--amber-soft); }
.stage-primary { color: var(--primary); background: var(--primary-soft); }
.stage-green   { color: var(--green);   background: var(--green-soft); }
.stage-red     { color: var(--red);     background: var(--red-soft); }
.stage-gray    { color: var(--gray);    background: var(--gray-soft); }

.stage-dot-blue    { background: var(--blue); }
.stage-dot-violet  { background: var(--violet); }
.stage-dot-amber   { background: var(--amber); }
.stage-dot-primary { background: var(--primary); }
.stage-dot-green   { background: var(--green); }
.stage-dot-red     { background: var(--red); }
.stage-dot-gray    { background: var(--gray); }

.stage-row {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 11px 18px;
	border-top: 1px solid var(--border);
	flex-wrap: wrap;
}

.stage-row:first-child {
	border-top: 0;
}

.stage-row.is-dragging {
	opacity: .45;
}

.stage-row.drop-before {
	border-top: 2px solid var(--primary);
}

.stage-row .dot {
	width: 10px;
	height: 10px;
	border-radius: 50%;
	flex-shrink: 0;
}

.stage-actions {
	display: flex;
	align-items: center;
	gap: 8px;
	margin-left: auto;
}

.color-picker {
	display: inline-flex;
	align-items: center;
	gap: 4px;
}

.color-swatch {
	width: 16px;
	height: 16px;
	border-radius: 50%;
	border: 2px solid transparent;
	cursor: pointer;
	padding: 0;
}

.color-swatch.is-current {
	border-color: var(--text);
}

.color-swatch input {
	position: absolute;
	opacity: 0;
	width: 0;
	height: 0;
}

.color-swatch:has(input:checked) {
	border-color: var(--text);
}

/* ---- detail ---- */

/* A two-column detail page is a design-system shape, not a leads shape: the
   calendar's event page is the same layout. The neutral names are canonical
   and the .lead-* spellings are kept as aliases so nothing that already uses
   them has to change. */
.detail-layout,
.lead-layout {
	display: grid;
	grid-template-columns: 1fr;
	gap: 14px;
	align-items: start;
}

@media (min-width: 1100px) {
	.detail-layout,
	.lead-layout {
		grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
	}
}

.detail-main,
.lead-main {
	display: grid;
	gap: 14px;
	min-width: 0;
}

.detail-fields,
.lead-fields {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 12px 18px;
}

.detail-fields > div,
.lead-fields > div {
	display: grid;
	gap: 2px;
	min-width: 0;
}

.detail-fields .k,
.lead-fields .k,
.lead-controls .k {
	font-size: 11px;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .4px;
	color: var(--text-3);
}

.detail-fields .v,
.lead-fields .v {
	font-size: 13px;
	overflow-wrap: anywhere;
}

.lead-controls {
	display: flex;
	align-items: flex-end;
	gap: 14px;
	flex-wrap: wrap;
	border-top: 1px solid var(--border);
}

.lead-controls form {
	display: grid;
	gap: 4px;
}

.lead-controls label {
	font-size: 11px;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .4px;
	color: var(--text-3);
}

/* ---- the activity trail ---- */

.note-form {
	display: flex;
	align-items: flex-start;
	gap: 8px;
	padding: 14px 18px;
	border-bottom: 1px solid var(--border);
}

.note-form textarea {
	flex: 1;
	min-height: 40px;
	resize: vertical;
}

.timeline {
	padding: 6px 18px 16px;
}

.timeline-item {
	display: flex;
	gap: 10px;
	padding: 10px 0;
	border-bottom: 1px solid var(--border);
}

.timeline-item:last-child {
	border-bottom: 0;
}

.timeline-dot {
	display: grid;
	place-items: center;
	width: 24px;
	height: 24px;
	border-radius: 50%;
	background: var(--bg);
	color: var(--text-3);
	flex-shrink: 0;
}

.timeline-item.is-note .timeline-dot {
	background: var(--primary-soft);
	color: var(--primary);
}

.timeline-body {
	min-width: 0;
	flex: 1;
}

.timeline-head {
	display: flex;
	align-items: baseline;
	gap: 8px;
	flex-wrap: wrap;
}

.timeline-head .who {
	font-size: 12.5px;
	font-weight: 600;
}

.timeline-body .note {
	margin: 3px 0 0;
	font-size: 13px;
	white-space: pre-wrap;
	overflow-wrap: anywhere;
}

.timeline-body .sys {
	margin: 2px 0 0;
	font-size: 12.5px;
	color: var(--text-2);
}

/* ---- widget rows: a stack of record links (leads, contacts, anything) ---- */

.recent-list {
	display: grid;
	gap: 2px;
}

.recent-row {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 7px 8px;
	border-radius: 8px;
	color: var(--text);
	text-decoration: none;
}

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

.recent-row .who {
	display: grid;
	gap: 1px;
	min-width: 0;
	flex: 1;
}

.recent-row .n {
	font-size: 13px;
	font-weight: 600;
}

.recent-row .b {
	font-size: 11.5px;
	color: var(--text-3);
}

.stage-bars {
	display: grid;
	gap: 7px;
}

.stage-bar {
	display: grid;
	grid-template-columns: 88px 1fr 32px;
	align-items: center;
	gap: 9px;
	color: var(--text);
	text-decoration: none;
	font-size: 12.5px;
}

.stage-bar:hover .lbl {
	color: var(--primary);
}

.stage-bar .lbl {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.stage-bar .track {
	height: 8px;
	border-radius: 5px;
	background: var(--bg);
	overflow: hidden;
}

.stage-bar .fill {
	display: block;
	height: 100%;
	border-radius: 5px;
	min-width: 2px;
}

.stage-bar .num {
	text-align: right;
	color: var(--text-3);
}

.stage-total {
	display: flex;
	align-items: center;
	justify-content: space-between;
	margin-top: 11px;
	padding-top: 9px;
	border-top: 1px solid var(--border);
	font-size: 12.5px;
	color: var(--text-3);
}

.filters {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
}

@media (max-width: 820px) {
	.detail-fields,
	.lead-fields {
		grid-template-columns: 1fr;
	}

	.kcol {
		max-height: none;
	}
}

/* ---------- Dashboard: the widget grid ---------- */

.widget-grid {
	display: grid;
	grid-template-columns: 1fr;
	gap: 14px;
	align-items: start;
}

/* Two columns once there is room; below that everything is full width, so a
   "wide" card and a normal one look the same — which is correct. */
@media (min-width: 1100px) {
	.widget-grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	.widget-card.is-wide {
		grid-column: 1 / -1;
	}
}

.widget-grid-empty {
	grid-column: 1 / -1;
}

.widget-card {
	display: flex;
	flex-direction: column;
	min-width: 0;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: var(--shadow-sm);
	transition: opacity .12s, border-color .12s;
}

.widget-card.is-dragging {
	opacity: .45;
}

.widget-card.drop-before {
	border-left: 3px solid var(--primary);
}

.widget-card.drop-after {
	border-right: 3px solid var(--primary);
}

.widget-card.htmx-request {
	opacity: .6;
}

.widget-card-head {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 11px 14px;
	border-bottom: 1px solid var(--border);
}

.widget-card-title {
	font-size: 13.5px;
	font-weight: 600;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.widget-card-actions {
	display: flex;
	align-items: center;
	gap: 2px;
	margin-left: auto;
	opacity: 0;
	transition: opacity .12s;
}

/* Controls appear on hover or keyboard focus — and are always there on touch,
   where there is no hover to reveal them. */
.widget-card:hover .widget-card-actions,
.widget-card:focus-within .widget-card-actions {
	opacity: 1;
}

@media (hover: none) {
	.widget-card-actions {
		opacity: 1;
	}
}

.widget-card-body {
	padding: 14px;
	min-width: 0;
}

/* ---- the picker ---- */

/* THE ROOT CAUSE OF THE MODAL OVERFLOW, and the line that fixes it.
 *
 * A grid track sizes itself to `auto`, which is `minmax(min-content, max-content)`
 * — so ONE row that cannot shrink widens EVERY row, and the whole list then
 * exceeds the 460px card. Measured before the fix: the admin's picker had a row
 * for "Recent bans / security_fail2ban / fail2ban.manage" whose min-content was
 * 486px, and all six rows were drawn 486 wide, each painting 46px past the
 * card's right edge. That is why the symptom looked like an innocent row — "by
 * stage / deals" — bleeding out: it was carried out by a neighbour.
 *
 * `minmax(0, 1fr)` lets the track shrink to the card, and the rules below let
 * the CONTENT shrink with it rather than spilling. */
.picker-list {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
}

/* A GRID, not a wrapping flex row, so the shape is the same on every row and
 * at every width: the name and key on line one, the module and permission on
 * line two, the spinner and the Add button on the right of both. A wrapping
 * flex row put the button on its own line for the rows with long metadata and
 * not for the others, which reads as a list that has come apart.
 *
 * The first track is `minmax(0, 1fr)` for the reason the list is: `auto` would
 * be min-content, and min-content is what put this content outside the modal. */
.picker-row {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto auto;
	align-items: center;
	column-gap: 8px;
	row-gap: 4px;
	padding: 11px 2px;
	border-bottom: 1px solid var(--border);
	min-width: 0;
}

.picker-row .picker-label {
	grid-column: 1;
	grid-row: 1;
}

.picker-meta {
	grid-column: 1;
	grid-row: 2;
	display: flex;
	align-items: center;
	gap: 6px;
	min-width: 0;
}

.picker-row .cell-spinner {
	grid-column: 2;
	grid-row: 1 / span 2;
}

.picker-row button[type="submit"] {
	grid-column: 3;
	grid-row: 1 / span 2;
	justify-self: end;
}

.picker-row:last-child {
	border-bottom: 0;
}

.picker-row button[type="submit"] {
	margin-left: auto;
}

.picker-label {
	display: grid;
	gap: 1px;
	min-width: 0;
}

/* Truncated rather than wrapped, because a widget key is one token — a wrapped
 * `security_fail2ban.status` is two lines of noise, and an ellipsis says
 * "there is more of this" in the space of a character. `min-width: 0` on the
 * parent lets them shrink; these make them stop at the edge. */
.picker-label .n,
.picker-label .mono {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.picker-label .n {
	font-weight: 600;
	font-size: 13px;
}

.picker-label .mono {
	font-size: 11px;
}

/* The module and the permission truncate rather than force the row wider —
 * a permission key is a token, and half of one with an ellipsis still tells
 * the reader which family it belongs to. */
.picker-meta .badge,
.picker-meta .tag {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* ---- the two shipped widgets ---- */

.welcome-links {
	display: grid;
	gap: 6px;
}

.welcome-link {
	display: flex;
	align-items: flex-start;
	gap: 10px;
	padding: 9px 10px;
	border-radius: 8px;
	color: var(--text);
	text-decoration: none;
}

.welcome-link:hover {
	background: var(--primary-soft);
	color: var(--primary);
}

.welcome-link > span {
	display: grid;
	gap: 1px;
	min-width: 0;
}

.welcome-link .n {
	font-weight: 600;
	font-size: 13px;
}

.welcome-link .b {
	font-size: 12px;
	color: var(--text-3);
}

.status-grid {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: 8px;
	margin-bottom: 12px;
}

.status-tile {
	display: grid;
	gap: 2px;
	padding: 9px 10px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--bg);
}

.status-tile.is-bad {
	border-color: var(--red);
	background: var(--red-soft);
}

.status-tile .label {
	font-size: 10.5px;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .4px;
	color: var(--text-3);
}

.status-tile .value {
	font-size: 19px;
	font-weight: 700;
	font-variant-numeric: tabular-nums;
}

.status-rows {
	display: grid;
}

.status-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 10px;
	padding: 8px 0;
	border-top: 1px solid var(--border);
	font-size: 13px;
}

.status-row .n {
	font-weight: 500;
}

@media (max-width: 520px) {
	.status-grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

/* ---------- Notifications: bell, dropdown, list, preferences ---------- */

.topbar-action {
	display: flex;
	align-items: center;
}

.bell {
	position: relative;
	display: flex;
	align-items: center;
}

/* The unread count, tucked into the corner of the bell icon. */
.bell-badge {
	position: absolute;
	top: 2px;
	right: 2px;
	min-width: 15px;
	height: 15px;
	padding: 0 4px;
	border-radius: 999px;
	background: var(--red);
	color: #fff;
	font-size: 9.5px;
	font-weight: 700;
	line-height: 15px;
	text-align: center;
	pointer-events: none;
}

.bell-badge[hidden] {
	display: none;
}

.bell-menu {
	position: absolute;
	top: calc(100% + 8px);
	right: 0;
	z-index: 40;
	width: 340px;
	max-width: calc(100vw - 24px);
	max-height: min(70vh, 460px);
	display: flex;
	flex-direction: column;
	overflow: hidden;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: var(--shadow-lg);
}

.bell-menu[hidden] {
	display: none;
}

/*
 * ===========================================================================
 * ON A NARROW SCREEN THE MENU HANGS FROM THE VIEWPORT, NOT FROM THE BELL
 * ===========================================================================
 *
 * `max-width: calc(100vw - 24px)` above constrains the SIZE and says nothing
 * about the POSITION, and the position was the bug. `right: 0` aligns the
 * menu's right edge with the bell's, and the bell is not at the right edge of
 * the screen — the Help button sits after it. Measured at 390px: the bell's
 * right edge is at x=326, the menu is 340px wide, so its left edge lands at
 * x=-14 and the first fourteen pixels of every notification are off-screen.
 * The max-width never bound, because 340 < 100vw-24 = 366.
 *
 * Anchoring to the viewport is what makes it correct regardless of what else is
 * in the topbar. `position: fixed` with both `left` and `right` set gives a
 * menu that is exactly as wide as the screen minus its margins, wherever the
 * bell happens to be, and it survives somebody adding another topbar action
 * tomorrow — which is what would otherwise re-break it.
 *
 * `top` comes off --topbar-h so the menu cannot drift away from the header it
 * belongs to. `position: fixed` is safe here because nothing between this and
 * the viewport creates a containing block: the topbar and its ancestors have no
 * transform, filter or will-change. (`.page` has an animation with a transform
 * — but that is inside `.content`, which is nowhere in this element's chain.)
 */
@media (max-width: 820px) {
	.bell-menu {
		position: fixed;
		top: calc(var(--topbar-h) + 8px);
		left: 8px;
		right: 8px;
		width: auto;
		max-width: none;
		max-height: calc(100dvh - var(--topbar-h) - 24px);
	}
}

/*
 * The message text itself. A notification body is arbitrary text — a subject
 * line, a phone number, a URL from an automation — and one long unbroken token
 * would otherwise widen the row past the menu it sits in and clip on the right
 * what the fix above just rescued on the left.
 */
.bell-item-body .n,
.bell-item-body .b {
	overflow-wrap: anywhere;
}

.bell-head,
.bell-foot {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 10px 14px;
	font-size: 12.5px;
}

.bell-head {
	border-bottom: 1px solid var(--border);
}

.bell-head .n {
	font-weight: 600;
}

.bell-head form {
	margin-left: auto;
}

.bell-foot {
	border-top: 1px solid var(--border);
	justify-content: center;
}

.bell-list {
	overflow-y: auto;
}

.bell-item {
	display: flex;
	align-items: flex-start;
	gap: 10px;
	padding: 11px 14px;
	border-bottom: 1px solid var(--border);
}

.bell-item:last-child {
	border-bottom: 0;
}

.bell-item.is-unread {
	background: var(--primary-soft);
}

.bell-item-icon {
	display: flex;
	padding-top: 1px;
	color: var(--text-2);
}

.bell-item-body {
	display: grid;
	gap: 2px;
	min-width: 0;
	flex: 1;
}

.bell-item-body .n {
	font-size: 13px;
	font-weight: 600;
	color: var(--text);
	text-decoration: none;
}

.bell-item-body a.n:hover {
	color: var(--primary);
}

.bell-item-body .b {
	font-size: 12px;
	color: var(--text-2);
}

.bell-item-body .t {
	font-size: 10.5px;
	color: var(--text-3);
}

.bell-empty {
	display: grid;
	justify-items: center;
	gap: 8px;
	padding: 28px 14px;
	color: var(--text-3);
	font-size: 12.5px;
}

/* ---- the list page ---- */

.notification-rows {
	display: grid;
}

.notification-row {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 12px 18px;
	border-top: 1px solid var(--border);
}

.notification-row.is-unread {
	background: var(--primary-soft);
}

.notification-icon {
	display: flex;
	color: var(--text-2);
}

.notification-body {
	display: grid;
	gap: 2px;
	min-width: 0;
	flex: 1;
}

.notification-body .n {
	font-weight: 600;
	color: var(--text);
	text-decoration: none;
}

.notification-body a.n:hover {
	color: var(--primary);
}

.notification-body .b {
	font-size: 12.5px;
	color: var(--text-2);
}

/* ---- preferences ---- */

.pref-head,
.pref-row {
	display: grid;
	grid-template-columns: 1fr 74px 74px 20px;
	align-items: center;
	gap: 8px;
	padding: 10px 18px;
}

.pref-head {
	border-top: 1px solid var(--border);
	padding-bottom: 4px;
}

.pref-head span:not(:first-child),
.pref-row .check-row {
	justify-self: center;
}

.pref-row {
	border-top: 1px solid var(--border);
}

.pref-label {
	display: grid;
	gap: 1px;
	min-width: 0;
}

.pref-label .n {
	font-weight: 600;
	font-size: 13px;
}

.pref-label .mono {
	font-size: 11px;
}

@media (max-width: 820px) {
	/*
	 * `.bell-menu` USED TO BE HERE, as `width: min(340px, calc(100vw - 24px))`,
	 * and it is worth recording why it was removed rather than adjusted.
	 *
	 * It was an earlier attempt at this same bug and it could not have worked:
	 * it constrains the menu's WIDTH, and the menu was never too wide. It is
	 * 340px, the constraint evaluates to 340px at every width above 364px, and
	 * the reason the text ran off the left edge was that the menu is anchored
	 * to the BELL — which is not at the edge of the screen — so its left edge
	 * landed at x=-14 on a 390px viewport. A width rule cannot move a box.
	 *
	 * Worse, sitting 200 lines below the real `.bell-menu` block and in a second
	 * media query of the same breakpoint, it silently won on `width` over the
	 * viewport anchoring added up there — so the first version of that fix
	 * produced a menu correctly positioned at left:8 and still 340px wide.
	 * The geometry now lives in exactly one place, next to the rule it modifies.
	 */
	.pref-head,
	.pref-row {
		grid-template-columns: 1fr 56px 56px 18px;
	}
}

/* ---------- Cross-module contracts (widgets + links) ---------- */

/* One module's contribution to another's zone. Kept plain on purpose: the
   host page owns the surrounding chrome, the widget owns only its contents. */
.widget-panel {
	border-top: 1px solid var(--border);
}

/* The card LinkRegistry builds for a linked entity. A module that wants a
   richer card returns its own HTML instead. */
.link-card {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 9px 12px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface);
	color: var(--text);
	text-decoration: none;
	font-size: 13px;
}

a.link-card:hover {
	border-color: var(--primary);
	background: var(--primary-soft);
	color: var(--primary);
}

.link-card .n {
	font-weight: 600;
}

/* The counterpart's module is disabled or gone — say so, quietly, instead of
   pretending the link is broken. */
.link-card.is-unavailable {
	border-style: dashed;
	background: var(--bg);
}

.link-card.is-unavailable .mono {
	margin-left: auto;
	font-size: 11px;
}

/* ---------- Avatars from files ---------- */

/* .avatar sets display:grid, which would otherwise beat the browser's own
   [hidden] rule and show the fallback badge next to the image. */
.avatar[hidden] {
	display: none;
}

.avatar-img {
	object-fit: cover;
	background: var(--surface-2);
}

/* ---------- List pagination ---------- */

.pager {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	padding: 10px 2px 0;
	flex-wrap: wrap;
}

.pager-count {
	font-size: 12px;
	color: var(--text-3);
	font-weight: 500;
}

.pager-pages {
	display: flex;
	align-items: center;
	gap: 4px;
}

.pager-btn {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	min-width: 30px;
	justify-content: center;
	padding: 4px 9px;
	border-radius: 7px;
	font-size: 12.5px;
	font-weight: 600;
	color: var(--text-2);
	background: var(--surface);
	border: 1px solid var(--border);
}

.pager-btn:hover {
	border-color: var(--border-strong);
	color: var(--text);
}

.pager-btn.is-current {
	background: var(--primary-soft);
	border-color: var(--primary-soft-border);
	color: var(--primary);
}

.pager-btn.is-disabled {
	opacity: .45;
	pointer-events: none;
}

.pager-gap {
	color: var(--text-3);
	padding: 0 2px;
}

/* Prev arrow reuses the chevron, rotated. */
.pager-btn:first-child .icon {
	transform: rotate(90deg);
}

/* ---------- List rows ---------- */

tbody tr.is-muted td {
	opacity: .6;
}

.seg a {
	padding: 4px 12px;
	border-radius: 6px;
	font-size: 12.5px;
	font-weight: 500;
	color: var(--text-2);
}

.seg a.on {
	background: var(--primary-soft);
	color: var(--primary);
	font-weight: 600;
}

.user-chip-link {
	display: flex;
	align-items: center;
	gap: 10px;
	min-width: 0;
	flex: 1;
}

/* ---------- Menu builder ---------- */

.menu-tree {
	padding: 10px 12px;
	transition: opacity .12s ease;
}

/* A tree-wide action (create, restore all) is in flight. */
.menu-tree.htmx-request {
	opacity: .55;
	pointer-events: none;
}

.menu-branch {
	list-style: none;
	margin: 0;
	padding: 0;
}

.menu-children .menu-branch {
	margin-left: 26px;
	padding-left: 10px;
	border-left: 1px solid var(--border);
	min-height: 6px;
}

.menu-row {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 7px 8px;
	border-radius: 8px;
	border: 1px solid transparent;
	background: var(--surface);
	cursor: grab;
}

.menu-row:hover {
	background: var(--surface-2);
	border-color: var(--border);
}

.menu-row.is-dragging {
	opacity: .45;
}

.menu-row.is-hidden {
	opacity: .55;
}

.menu-row.is-disabled-module {
	background: var(--amber-soft);
}

.menu-row.drop-into {
	border-color: var(--primary);
	background: var(--primary-soft);
}

.menu-node.drop-before > .menu-row {
	box-shadow: 0 -2px 0 0 var(--primary);
}

.menu-node.drop-after > .menu-row {
	box-shadow: 0 2px 0 0 var(--primary);
}

.drag-handle {
	color: var(--text-3);
	cursor: grab;
	display: grid;
	place-items: center;
}

.menu-icon {
	width: 20px;
	display: grid;
	place-items: center;
	color: var(--text-2);
}

.menu-meta {
	display: flex;
	align-items: center;
	gap: 7px;
	flex-wrap: wrap;
	font-size: 11.5px;
	color: var(--text-3);
	min-width: 0;
}

.menu-actions {
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: 2px;
	flex-shrink: 0;
}

/* ---------- Icon picker ---------- */

.icon-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
	gap: 6px;
	max-height: 46vh;
	overflow-y: auto;
}

.icon-option {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 5px;
	padding: 9px 4px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface);
	color: var(--text-2);
}

/* The filter hides options with [hidden]; display: flex above outranks the
   user-agent rule, so say it again. */
.icon-option[hidden] {
	display: none;
}

.icon-option:hover {
	border-color: var(--primary);
	background: var(--primary-soft);
	color: var(--primary);
}

.icon-option .mono {
	font-size: 10px;
	text-align: center;
	word-break: break-all;
	line-height: 1.2;
}

/* ---------- Leads: custom field definitions ---------- */

.def-row {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 11px 18px;
	border-top: 1px solid var(--border);
	flex-wrap: wrap;
}

.def-row:first-child {
	border-top: 0;
}

.def-row.is-dragging {
	opacity: .45;
}

.def-row.drop-before {
	border-top: 2px solid var(--primary);
}

.def-name {
	display: grid;
	gap: 1px;
	min-width: 0;
}

.def-name .nm {
	font-size: 13px;
	font-weight: 600;
}

.def-name .em {
	font-size: 11px;
	color: var(--text-3);
}

.def-flags {
	display: flex;
	align-items: center;
	gap: 14px;
}

.def-flags .lbl {
	font-size: 11.5px;
	color: var(--text-2);
}

.def-caps {
	font-size: 11px;
}

/* Key and type stop being editable once a value exists under them. */
.field.is-locked label {
	display: flex;
	align-items: center;
	gap: 5px;
	color: var(--text-3);
}

.field.is-locked input:disabled {
	background: var(--surface-2);
	color: var(--text-2);
	cursor: not-allowed;
}

.option-editor {
	gap: 8px;
}

/* [hidden] on a grid/flex field loses to the rule above it; say it again. */
.option-editor[hidden] {
	display: none;
}

.option-rows {
	display: grid;
	gap: 6px;
}

.option-row {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
	gap: 6px;
	align-items: center;
}

.field-flags,
.checkbox-group {
	display: flex;
	flex-wrap: wrap;
	gap: 8px 18px;
}

.flag {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	font-size: 12.5px;
	cursor: pointer;
}

.flag input[type="checkbox"] {
	width: 16px;
	height: 16px;
	accent-color: var(--primary);
	cursor: pointer;
}

/* ---------- Leads: the custom half of the form ---------- */

/* The custom-fields block is a .form-section that also draws a rule — it is the
   grouped block on New deal, New lead, New contact and New company, so it takes
   the section spacing from the same knobs rather than keeping its own numbers.
   (It used to say gap 13 / padding-top 15 / margin-top 2, which is three
   numbers nobody could have derived from anything.) */
.custom-fields {
	display: grid;
	gap: 16px;
	padding-top: 22px;
	margin-top: 6px;
	border-top: 1px solid var(--border);
}

.custom-fields-head {
	font-size: 11px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: .4px;
	color: var(--text-3);
}

.custom-fields .req {
	color: var(--red);
	margin-left: 2px;
}

@media (max-width: 820px) {
	.def-row {
		align-items: flex-start;
	}

	.def-flags {
		gap: 12px;
	}

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

	/* Label on its own line, key and the remove button sharing the next. */
	.option-row [data-option-label] {
		grid-column: 1 / -1;
	}
}

/* ---------- Leads: the import wizard ---------- */

.wizard-steps {
	display: flex;
	align-items: center;
	gap: 6px;
	flex-wrap: wrap;
	list-style: none;
	margin: 0 0 4px;
	padding: 0;
}

.wizard-step {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	padding: 6px 12px 6px 8px;
	border: 1px solid var(--border);
	border-radius: 999px;
	background: var(--surface);
	font-size: 12.5px;
	color: var(--text-3);
}

.wizard-step .n {
	display: grid;
	place-items: center;
	width: 19px;
	height: 19px;
	border-radius: 50%;
	background: var(--surface-2);
	font-size: 11px;
	font-weight: 700;
	line-height: 0;
}

.wizard-step.is-current {
	border-color: var(--primary);
	background: var(--primary-soft);
	color: var(--primary);
}

.wizard-step.is-current .n {
	background: var(--primary);
	color: #fff;
}

.wizard-step.is-done {
	color: var(--green);
	border-color: var(--green-soft);
}

.wizard-step.is-done .n {
	background: var(--green-soft);
	color: var(--green);
}

/* ---- step 1: the dropzone ---- */

.dropzone {
	display: grid;
	justify-items: center;
	gap: 6px;
	padding: 26px 18px;
	border: 1.5px dashed var(--border);
	border-radius: 12px;
	background: var(--surface-2);
	color: var(--text-2);
	cursor: pointer;
	text-align: center;
	transition: border-color .14s, background .14s;
}

.dropzone:hover,
.dropzone.is-over {
	border-color: var(--primary);
	background: var(--primary-soft);
	color: var(--primary);
}

.dropzone.has-file {
	border-style: solid;
	border-color: var(--green);
}

.dropzone .hint {
	max-width: 44ch;
}

/* The input stays focusable — hiding it with display:none would take it out
   of the tab order and the label with it. */
.dropzone input[type="file"] {
	position: absolute;
	width: 1px;
	height: 1px;
	opacity: 0;
	pointer-events: none;
}

/* ---- step 2: the column cards ---- */

.map-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
	gap: 12px;
}

.map-col {
	display: grid;
	gap: 7px;
	padding: 12px;
	border: 1px solid var(--border);
	border-radius: 10px;
	background: var(--surface);
	min-width: 0;
}

.map-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 8px;
	min-width: 0;
}

.map-head .nm {
	font-size: 13px;
	font-weight: 600;
	overflow-wrap: anywhere;
}

.map-sample {
	display: grid;
	gap: 2px;
	font-size: 11px;
	color: var(--text-3);
	min-height: 16px;
}

.map-sample span {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.map-new {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
	gap: 6px;
}

/* [hidden] loses to the grid above it; say it again. */
.map-new[hidden] {
	display: none;
}

/* ---- step 3: the preview ---- */

.projection {
	margin: 0;
	font-size: 14px;
	line-height: 1.8;
}

.projection b {
	font-size: 15px;
}

td.cell-bad {
	background: var(--red-soft);
	vertical-align: top;
}

.cell-why {
	display: flex;
	align-items: flex-start;
	gap: 4px;
	margin-top: 3px;
	font-size: 11px;
	color: var(--red);
	max-width: 26ch;
}

/* ---- step 4: progress ---- */

.progress-track {
	height: 8px;
	border-radius: 999px;
	background: var(--surface-2);
	overflow: hidden;
}

.progress-fill {
	height: 100%;
	background: var(--primary);
	border-radius: 999px;
	transition: width .3s ease;
}

.progress-legend {
	display: flex;
	justify-content: space-between;
	gap: 10px;
	margin-top: 7px;
	font-size: 11.5px;
	color: var(--text-3);
}

.import-counts {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: 12px;
	margin-top: 16px;
}

.import-counts > div {
	display: grid;
	gap: 2px;
}

.import-counts .k {
	font-size: 11px;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .4px;
	color: var(--text-3);
}

.import-counts .v {
	font-size: 19px;
	font-weight: 700;
}

@media (max-width: 820px) {
	.import-counts {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	.map-new {
		grid-template-columns: 1fr;
	}

	.wizard-step .t {
		display: none;
	}

	.wizard-step.is-current .t {
		display: inline;
	}
}

/* ---------- Calendar ---------- */

.cal {
	display: grid;
	gap: 12px;
}

.cal-bar {
	display: flex;
	align-items: center;
	gap: 12px;
	flex-wrap: wrap;
}

.cal-nav {
	display: flex;
	align-items: center;
	gap: 6px;
}

/* The sprite's chevron points down; prev and next turn it. */
.cal-prev .icon { transform: rotate(90deg); }
.cal-next .icon { transform: none; }

.cal-title {
	font-size: 15px;
	font-weight: 700;
	margin-left: 8px;
}

.cal-views {
	display: inline-flex;
	border: 1px solid var(--border);
	border-radius: 8px;
	overflow: hidden;
	margin-left: auto;
}

.cal-view-btn {
	padding: 6px 13px;
	font-size: 12.5px;
	font-weight: 600;
	color: var(--text-2);
	background: var(--surface);
	border-right: 1px solid var(--border);
}

.cal-view-btn:last-child { border-right: 0; }

.cal-view-btn.is-current {
	background: var(--primary);
	color: var(--on-primary);
}

/* ---- month ---- */

.cal-month {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: var(--shadow-sm);
	overflow: hidden;
}

.cal-month-head,
.cal-month-body {
	display: grid;
	grid-template-columns: repeat(7, minmax(0, 1fr));
}

.cal-month-head > div {
	padding: 8px 10px;
	background: var(--surface-2);
	border-bottom: 1px solid var(--border);
	font-size: 11px;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .5px;
	color: var(--text-3);
}

.cal-day {
	min-height: 108px;
	border-right: 1px solid var(--border);
	border-bottom: 1px solid var(--border);
	padding: 4px;
	display: grid;
	grid-template-rows: auto 1fr;
	gap: 3px;
	min-width: 0;
}

.cal-day:nth-child(7n) { border-right: 0; }
.cal-day.is-outside { background: var(--surface-2); }
.cal-day.is-outside .cal-day-num { color: var(--text-3); }
.cal-day.dragover { background: var(--primary-soft); }

.cal-day-head {
	display: flex;
	justify-content: flex-end;
}

.cal-day-num {
	font-size: 12px;
	font-weight: 600;
	color: var(--text-2);
	width: 22px;
	height: 22px;
	border-radius: 50%;
	display: grid;
	place-items: center;
}

.cal-day.is-today .cal-day-num {
	background: var(--primary);
	color: var(--on-primary);
}

.cal-day-body {
	display: grid;
	align-content: start;
	gap: 2px;
	min-width: 0;
}

.cal-more {
	font-size: 11px;
	font-weight: 600;
	color: var(--text-3);
	text-align: left;
	padding: 1px 4px;
}

.cal-more:hover { color: var(--primary); }

/* ---- week / day ---- */

.cal-grid {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: var(--shadow-sm);
	overflow: hidden;
}

.cal-grid-head,
.cal-allday,
.cal-grid-body {
	display: grid;
	grid-template-columns: 56px repeat(var(--cal-cols, 7), minmax(0, 1fr));
}

/* The day view is the same grid with one column. */
.cal-grid.is-day {
	--cal-cols: 1;
}

.cal-grid-head > div {
	padding: 8px 6px;
	background: var(--surface-2);
	border-bottom: 1px solid var(--border);
	text-align: center;
}

.cal-col-head .d {
	display: block;
	font-size: 10.5px;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .5px;
	color: var(--text-3);
}

.cal-col-head .n {
	font-size: 15px;
	font-weight: 700;
}

.cal-col-head.is-today .n { color: var(--primary); }

.cal-allday { border-bottom: 1px solid var(--border); }

.cal-allday-col {
	padding: 3px;
	border-left: 1px solid var(--border);
	display: grid;
	gap: 2px;
	align-content: start;
	min-height: 26px;
}

.cal-allday .cal-gutter-head {
	display: grid;
	place-items: center;
}

/* One hour is a fixed height, so a 15-minute event is 11px rather than a
   sliver, and every position below is that height times a whole number. */
.cal-grid {
	--hour-h: 44px;
}

.cal-grid-body {
	position: relative;
	max-height: 620px;
	overflow-y: auto;
}

.cal-gutter,
.cal-col {
	height: calc(var(--hour-h) * 24);
}

.cal-gutter {
	position: relative;
	border-right: 1px solid var(--border);
}

.cal-hour {
	position: absolute;
	left: 0;
	right: 4px;
	top: calc(var(--hour) * var(--hour-h));
	transform: translateY(-50%);
	text-align: right;
	font-size: 10.5px;
	font-family: var(--mono);
	color: var(--text-3);
}

.cal-col {
	position: relative;
	border-left: 1px solid var(--border);
	min-height: 100%;
}

.cal-col.is-today { background: var(--primary-soft); }
.cal-col.dragover { background: var(--primary-soft); }

.cal-rule {
	position: absolute;
	left: 0;
	right: 0;
	top: calc(var(--hour) * var(--hour-h));
	border-top: 1px solid var(--border);
	pointer-events: none;
}

/* ---- chips ---- */

.cal-chip {
	position: relative;
	border-radius: 6px;
	overflow: hidden;
	font-size: 11.5px;
	min-width: 0;
}

.cal-grid .cal-chip {
	position: absolute;
	top: calc(var(--top) * var(--hour-h) / 60);
	height: calc(var(--mins) * var(--hour-h) / 60);
	/* Even a five-minute event has to show its title and be clickable. */
	min-height: 24px;
	left: calc(var(--col) * 100% / var(--cols));
	width: calc(100% / var(--cols) - 2px);
	margin-left: 1px;
}

.cal-chip-link {
	display: grid;
	gap: 1px;
	padding: 3px 6px;
	height: 100%;
	align-content: start;
	min-width: 0;
}

.cal-chip.is-compact .cal-chip-link {
	display: flex;
	align-items: center;
	gap: 5px;
	padding: 2px 6px;
	white-space: nowrap;
	overflow: hidden;
}

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

.cal-chip .t,
.cal-chip .lc {
	font-size: 10.5px;
	opacity: .8;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.cal-chip .rp {
	position: absolute;
	top: 2px;
	right: 3px;
	opacity: .7;
	line-height: 0;
}

.cal-chip.is-dragging { opacity: .45; }
.cal-chip[data-draggable="1"] { cursor: grab; }

.cal-resize {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 6px;
	cursor: ns-resize;
}

.cal-primary { background: var(--primary-soft); color: var(--primary); }
.cal-blue    { background: var(--blue-soft);    color: var(--blue); }
.cal-violet  { background: var(--violet-soft);  color: var(--violet); }
.cal-amber   { background: var(--amber-soft);   color: var(--amber); }
.cal-green   { background: var(--green-soft);   color: var(--green); }
.cal-red     { background: var(--red-soft);     color: var(--red); }

.cal-nexts {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	margin-top: 6px;
}

/* ---- event page ---- */

.respond-bar {
	display: flex;
	align-items: center;
	gap: 12px;
	flex-wrap: wrap;
	border-bottom: 1px solid var(--border);
}

.respond-bar form {
	display: flex;
	gap: 6px;
}

.attendee-list {
	display: grid;
	gap: 10px;
}

.attendee {
	display: flex;
	align-items: center;
	gap: 10px;
}

.attendee .who {
	display: grid;
	gap: 1px;
	min-width: 0;
}

.attendee .who .nm {
	font-size: 13px;
	font-weight: 600;
}

.attendee .who .em {
	font-size: 11px;
	color: var(--text-3);
}

.attendee .badge { margin-left: auto; }

.link-cards {
	display: grid;
	gap: 8px;
}

.link-card {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 8px 10px;
	border: 1px solid var(--border);
	border-radius: 8px;
	min-width: 0;
}

.link-card .nm {
	font-size: 12.5px;
	font-weight: 600;
	overflow-wrap: anywhere;
}

.link-card .em {
	font-size: 10.5px;
	color: var(--text-3);
}

.link-card form { margin-left: auto; }

.link-card.is-unavailable {
	border-style: dashed;
	opacity: .7;
}

.entity-hits {
	display: grid;
	gap: 4px;
	margin-top: 8px;
}

.entity-hit {
	display: flex;
	align-items: center;
	gap: 8px;
	width: 100%;
	padding: 6px 8px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface);
	text-align: left;
	font-size: 12.5px;
}

.entity-hit:hover {
	border-color: var(--primary);
	background: var(--primary-soft);
}

.entity-hit .nm { font-weight: 600; }

.entity-hit .em {
	color: var(--text-3);
	font-size: 11px;
}

.entity-hit .icon { margin-left: auto; }

/* ---- the repeat builder ---- */

.repeat-builder { gap: 8px; }

.repeat-row {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
}

.repeat-row input[type="number"] { width: 74px; }
.repeat-row input[type="date"] { width: auto; }

.repeat-part {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	font-size: 12.5px;
	color: var(--text-2);
}

/* [hidden] loses to the display rules above it; say it again. */
.repeat-part[hidden],
.repeat-row[hidden],
.repeat-days[hidden],
.time-row[hidden] {
	display: none;
}

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

.day-toggle { cursor: pointer; }

.day-toggle input {
	position: absolute;
	opacity: 0;
	width: 0;
	height: 0;
}

.day-toggle span {
	display: grid;
	place-items: center;
	width: 34px;
	height: 30px;
	border: 1px solid var(--border);
	border-radius: 7px;
	font-size: 11.5px;
	font-weight: 600;
	color: var(--text-2);
}

.day-toggle input:checked + span {
	background: var(--primary);
	border-color: var(--primary);
	color: var(--on-primary);
}

.time-row {
	display: flex;
	align-items: center;
	gap: 8px;
}

.feed-row {
	display: flex;
	gap: 8px;
}

.feed-row input {
	flex: 1;
	min-width: 0;
}

/* ---- upcoming lists (both calendar widgets) ---- */

.side-heading {
	font-size: 11px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: .4px;
	color: var(--text-3);
	margin: 12px 0 6px;
}

.side-heading:first-child { margin-top: 0; }

.upcoming-list {
	display: grid;
	gap: 6px;
}

.upcoming-list.is-past { opacity: .68; }

.upcoming-item {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 7px 9px;
	border-radius: 8px;
	min-width: 0;
}

.upcoming-item:hover { background: var(--surface-2); }

.upcoming-item .when {
	display: grid;
	gap: 1px;
	padding: 4px 8px;
	border-radius: 7px;
	text-align: center;
	min-width: 68px;
}

.upcoming-item .when .d {
	font-size: 10.5px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: .3px;
}

.upcoming-item .when .t { font-size: 11px; }

.upcoming-item .what {
	display: grid;
	gap: 1px;
	min-width: 0;
}

.upcoming-item .what .nm {
	font-size: 12.5px;
	font-weight: 600;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.upcoming-item .what .em {
	font-size: 11px;
	color: var(--text-3);
}

.upcoming-item .rp {
	margin-left: auto;
	color: var(--text-3);
	line-height: 0;
}

/* ---------- A follow-up that wants attention ----------
   Two states, and the difference between them is the whole reason the entity
   widget was worth rewriting: "this was due and you have not done it" and
   "this is due today" are the only two entries in that list anybody needs to
   see immediately, and before this they looked exactly like a date in March.

   A left border rather than a background, so an emphasised row still reads as
   one row in a list rather than as a card that has escaped from somewhere. */
.upcoming-item.is-overdue,
.upcoming-item.is-today {
	border-left: 2px solid transparent;
	padding-left: 7px;
}

.upcoming-item.is-overdue {
	border-left-color: var(--red);
	background: var(--red-soft);
}

.upcoming-item.is-overdue .when .d {
	color: var(--red);
}

.upcoming-item.is-today {
	border-left-color: var(--primary);
	background: var(--primary-soft);
}

.upcoming-item.is-today .when .d {
	color: var(--primary);
}

.upcoming-item .what .tag {
	justify-self: start;
}

/* The empty state's call to action. The button comes from the zone, so this
   only has to give it room. */
.empty-action {
	margin-top: 10px;
	display: flex;
	justify-content: center;
}

/* A sidebar card's closing line — one quiet link out to the full view. */
.side-foot {
	padding: 8px 2px 2px;
	font-size: 12px;
}

.side-foot a {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	color: var(--text-2);
	font-weight: 500;
}

.side-foot a:hover {
	color: var(--primary);
}

@media (max-width: 820px) {
	.cal-day { min-height: 76px; }

	.cal-title {
		margin-left: 0;
		width: 100%;
		order: 3;
	}

	.cal-views { margin-left: 0; }

	.cal-grid-body { max-height: 460px; }

	.cal-grid { --hour-h: 40px; }

	.cal-chip .lc { display: none; }
}

/* ---------- Mail: accounts, list, thread, and the cage ---------- */

.account-list {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
	gap: 14px;
}

.account-card .card-foot {
	display: flex;
	gap: 6px;
	flex-wrap: wrap;
	align-items: center;
}

.account-card .card-foot form {
	display: inline-flex;
}

.field-group-title {
	display: flex;
	align-items: center;
	gap: 6px;
	margin: 18px 0 10px;
	padding-top: 14px;
	border-top: 1px solid var(--border);
	font-size: 11px;
	font-weight: 700;
	letter-spacing: .06em;
	text-transform: uppercase;
	color: var(--text-3);
}

.test-row {
	display: flex;
	align-items: center;
	gap: 10px;
	flex-wrap: wrap;
}

.test-row .hint {
	flex: 1 1 240px;
	margin: 0;
}

.test-result {
	display: grid;
	gap: 6px;
	margin-top: 10px;
}

.test-line {
	display: flex;
	align-items: flex-start;
	gap: 8px;
	font-size: 12.5px;
	line-height: 1.5;
	padding: 8px 10px;
	border-radius: 8px;
	background: var(--surface-2);
}

.test-line.is-ok {
	color: var(--green);
	background: var(--green-soft);
}

.test-line.is-bad {
	color: var(--red);
	background: var(--red-soft);
}

.test-line.is-skipped {
	color: var(--text-3);
}

/* ---- the conversation list ---- */

.mail-list {
	list-style: none;
	margin: 0;
	padding: 0;
}

.mail-row + .mail-row {
	border-top: 1px solid var(--border);
}

.mail-row > a {
	display: grid;
	grid-template-columns: 210px minmax(0, 1fr) 132px;
	grid-template-areas: "who subject when" "who snippet when";
	align-items: center;
	gap: 2px 14px;
	padding: 11px 14px;
	color: inherit;
}

.mail-row > a:hover {
	background: var(--surface-2);
}

.mail-who {
	grid-area: who;
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: 12.5px;
	color: var(--text-2);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.mail-subject {
	grid-area: subject;
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: 13px;
	color: var(--text);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.mail-snippet {
	grid-area: snippet;
	font-size: 12px;
	color: var(--text-3);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.mail-when {
	grid-area: when;
	text-align: right;
	font-size: 11.5px;
}

.mail-count {
	font-size: 11px;
	font-weight: 700;
	color: var(--text-3);
	border: 1px solid var(--border);
	border-radius: 999px;
	padding: 0 6px;
}

.mail-row.is-unread .mail-who,
.mail-row.is-unread .mail-subject {
	font-weight: 700;
	color: var(--text);
}

.unread-pip {
	display: inline-block;
	min-width: 16px;
	padding: 0 4px;
	border-radius: 999px;
	background: var(--primary);
	color: #fff;
	font-size: 10.5px;
	line-height: 16px;
	text-align: center;
}

/* ---- one thread ---- */

.thread {
	display: grid;
	gap: 10px;
}

.thread-head {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 11px 14px;
	color: inherit;
}

.thread-head:hover {
	background: var(--surface-2);
}

.thread-who {
	display: flex;
	flex-direction: column;
	min-width: 0;
	margin-right: auto;
}

.thread-who b {
	font-size: 13px;
}

.thread-who .mono {
	font-size: 11px;
}

.thread-peek {
	padding: 0 14px 12px 54px;
	font-size: 12px;
	color: var(--text-3);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.thread-message.is-open .thread-head {
	border-bottom: 1px solid var(--border);
}

.message-meta {
	margin-bottom: 12px;
}

.images-blocked {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	padding: 8px 10px;
	margin-bottom: 12px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface-2);
	font-size: 12px;
	color: var(--text-2);
}

.images-blocked span {
	flex: 1 1 200px;
}

/* ---- the cage ---- */

.mail-frame-wrap {
	position: relative;
	--frame-h: 420px;
}

.mail-frame-wrap.is-tall {
	--frame-h: 80vh;
}

.mail-frame {
	display: block;
	width: 100%;
	height: var(--frame-h);
	border: 1px solid var(--border);
	border-radius: 8px;
	background: #fff;
}

.mail-frame-toggle {
	position: absolute;
	top: 6px;
	right: 6px;
	background: var(--surface);
	border: 1px solid var(--border);
}

.mail-text {
	margin: 0;
	padding: 12px 14px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface-2);
	font-size: 12.5px;
	line-height: 1.6;
	white-space: pre-wrap;
	word-break: break-word;
	overflow-x: auto;
}

.attachments {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	margin-top: 12px;
}

.attachment-chip {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	max-width: 100%;
	padding: 6px 10px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface-2);
	font-size: 12px;
	color: var(--text-2);
}

.attachment-chip:hover {
	border-color: var(--primary);
	color: var(--primary);
}

.attachment-chip .name {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* ---- compose, and the sidebar widget ---- */

.message-actions {
	display: flex;
	gap: 6px;
	margin-top: 12px;
}

.mail-mini {
	display: grid;
	gap: 2px;
}

.mail-mini-item {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 10px;
	padding: 7px 8px;
	border-radius: 8px;
	color: inherit;
	font-size: 12.5px;
}

.mail-mini-item:hover {
	background: var(--surface-2);
}

.mail-mini-item .s {
	display: flex;
	align-items: center;
	gap: 6px;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.mail-mini-item .m {
	display: flex;
	align-items: center;
	gap: 6px;
	flex: none;
	font-size: 11px;
}

.mail-mini-item.is-unread .s {
	font-weight: 700;
}

.mail-mini-foot {
	margin-top: 10px;
}

@media (max-width: 820px) {
	/*
		The mail screens carry more in the head than most — an account switcher
		as well as the usual buttons — and the shell's page-head does not wrap,
		so on a phone the title block was being squeezed to a five-word column.
		Scoped to these pages rather than changed globally: every other screen
		is fine as it is, and a shell change is not this phase's to make.
	*/
	.mail-head {
		flex-wrap: wrap;
	}

	.mail-head > div,
	.mail-head .page-actions {
		flex: 1 1 100%;
		flex-wrap: wrap;
	}

	.account-list {
		grid-template-columns: 1fr;
	}

	.mail-row > a {
		grid-template-columns: minmax(0, 1fr) auto;
		grid-template-areas: "who when" "subject subject" "snippet snippet";
		gap: 3px 10px;
	}

	.mail-frame-wrap {
		--frame-h: 320px;
	}

	.thread-peek {
		padding-left: 14px;
	}
}

/* ---------- Automation: the rule builder and the run log ---------- */

.repeater {
	margin-bottom: 6px;
}

.repeater-rows {
	display: grid;
	gap: 8px;
	margin-bottom: 8px;
}

.repeater-row {
	display: grid;
	grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1.4fr) auto;
	gap: 8px;
	align-items: center;
	padding: 8px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface-2);
}

.repeater-row.has-error {
	border-color: var(--red);
}

.repeater-row .error {
	grid-column: 1 / -1;
}

.action-row {
	display: block;
}

.action-head {
	display: flex;
	align-items: center;
	gap: 8px;
}

.action-head select {
	flex: 1;
	min-width: 0;
}

.action-position {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 20px;
	height: 20px;
	flex: none;
	border-radius: 50%;
	background: var(--primary-soft);
	color: var(--primary);
	font-size: 11px;
	font-weight: 700;
}

.action-row.is-dragging {
	opacity: .5;
}

.action-config {
	margin-top: 10px;
	padding-left: 30px;
}

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

/* ---- the payload sample and its clickable hints ---- */

.sample-box {
	margin: -6px 0 16px;
	padding: 10px 12px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface-2);
}

.sample-box .hint {
	margin: 0;
}

.sample-fields {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	margin-top: 8px;
}

.sample-field {
	display: inline-flex;
	align-items: baseline;
	gap: 6px;
	padding: 3px 9px;
	border: 1px solid var(--border);
	border-radius: 999px;
	background: var(--surface);
	font-size: 11.5px;
	cursor: pointer;
}

.sample-field:hover {
	border-color: var(--primary);
	color: var(--primary);
}

.sample-field .muted {
	font-size: 10.5px;
}

/* ---- the dry run ---- */

.dry-run {
	border-top: 1px solid var(--border);
	background: var(--surface-2);
}

.dry-run .test-line {
	background: var(--surface);
	align-items: center;
}

/* ---- the run log ---- */

.detail-grid {
	display: grid;
	grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
	gap: 14px;
	align-items: start;
}

.run-log {
	display: grid;
	gap: 0;
}

.run-line {
	display: grid;
	grid-template-columns: 18px minmax(0, 1fr);
	gap: 10px;
	padding: 0 0 16px;
	position: relative;
}

.run-line:not(:last-child)::before {
	content: '';
	position: absolute;
	left: 8px;
	top: 16px;
	bottom: 0;
	width: 1px;
	background: var(--border);
}

.run-dot {
	width: 9px;
	height: 9px;
	margin: 5px 0 0 4px;
	border-radius: 50%;
	background: var(--text-3);
	z-index: 1;
}

.run-line.is-ok .run-dot,
.run-line.is-matched .run-dot { background: var(--green); }
.run-line.is-failed .run-dot { background: var(--red); }
.run-line.is-waiting .run-dot { background: var(--amber); }
.run-line.is-suppressed .run-dot { background: var(--amber); }
.run-line.is-skipped .run-dot { background: var(--border-strong); }

.run-head {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
}

.run-action {
	font-size: 12.5px;
	font-weight: 600;
}

.run-message {
	margin-top: 3px;
	font-size: 12.5px;
	color: var(--text-2);
	line-height: 1.5;
	word-break: break-word;
}

.run-line.is-skipped .run-message {
	color: var(--text-3);
}

.payload-json {
	margin: 0;
	font-size: 12px;
	line-height: 1.55;
	white-space: pre-wrap;
	word-break: break-word;
	overflow-x: auto;
}

@media (max-width: 820px) {
	.detail-grid {
		grid-template-columns: 1fr;
	}

	.repeater-row {
		grid-template-columns: 1fr auto;
	}

	.repeater-row [data-operator],
	.repeater-row [data-value] {
		grid-column: 1 / -1;
	}

	.action-config {
		padding-left: 0;
	}
}

/* ---------- List navigation: ← Prev · 12 of 67 · Next → ----------
   The strip a detail page shows when it was opened from a list. Core chrome,
   like the pagination it is the counterpart to — four entity modules share it,
   so it is styled once here rather than four times. */
.list-nav {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	padding-right: 10px;
	margin-right: 6px;
	border-right: 1px solid var(--border);
}

.list-nav-pos {
	font-size: 12px;
	color: var(--text-3);
	padding: 0 4px;
	white-space: nowrap;
}

.list-nav-state {
	font-size: 12px;
	color: var(--amber);
	font-weight: 500;
	padding: 0 6px;
	white-space: nowrap;
}

/* Disabled, not hidden, at the ends of the list. A control that vanishes
   moves everything beside it and leaves you clicking whatever slid under the
   cursor; one that greys out stays where your eye left it. */
.btn.is-disabled {
	opacity: .45;
	pointer-events: none;
	cursor: default;
}

@media (max-width: 820px) {
	/* On a phone the strip wraps to its own line above the actions rather than
	   squeezing them; the divider would be pointing at nothing there. */
	.list-nav {
		border-right: 0;
		padding-right: 0;
		margin-right: 0;
		width: 100%;
	}
}

/* ---------- Firewall: rules, diff, the countdown ---------- */

.rule-list {
	display: flex;
	flex-direction: column;
}

.rule-row {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 9px 14px;
	border-bottom: 1px solid var(--border);
}

.rule-row:last-child {
	border-bottom: 0;
}

/* A disabled rule is still shown — knowing it exists and is off is the point
   of being able to switch it off rather than delete it. */
.rule-row.is-off .rule-spec,
.rule-row.is-off .rule-comment {
	opacity: .5;
	text-decoration: line-through;
}

.rule-spec {
	flex: 1;
	min-width: 0;
	font-size: 12.5px;
}

/* min-width: 0 is load-bearing, not decoration. A nowrap flex item has a
   min-content width equal to its whole text, and `min-width: auto` (the flex
   default) refuses to shrink past it — so at 390px the comment held its full
   width, the rule spec got what was left, and the actual rule rendered one word
   per line underneath it. */
.rule-comment {
	flex: 0 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-size: 12px;
}

.rule-actions {
	display: flex;
	gap: 2px;
}

.rule-actions form {
	display: contents;
}

.diff-box {
	margin: 0;
	padding: 10px 12px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface-2);
	font-size: 12px;
	line-height: 1.55;
	white-space: pre-wrap;
	word-break: break-word;
	max-height: 340px;
	overflow: auto;
}

.plan-details {
	margin-top: 10px;
}

.plan-details summary {
	cursor: pointer;
	font-size: 12.5px;
	color: var(--text-2);
	padding: 4px 0;
}

/* The apply card while a revert is armed. Red, because this is the one screen
   in the application where not reading it has consequences measured in a
   datacentre visit. */
.danger-card {
	border-color: var(--red);
	box-shadow: 0 0 0 3px var(--red-soft);
}

.countdown {
	display: flex;
	align-items: baseline;
	gap: 8px;
}

.countdown-value {
	font-size: 34px;
	font-weight: 700;
	line-height: 1;
	color: var(--red);
	font-variant-numeric: tabular-nums;
}

.countdown-unit {
	font-size: 12.5px;
	color: var(--text-2);
}

.btn-danger {
	background: var(--red);
	border-color: var(--red);
	color: #fff;
}

.btn-danger:hover:not(:disabled) {
	filter: brightness(.94);
}

.badge.warn {
	background: var(--amber-soft, var(--surface-2));
	color: var(--amber, var(--text-2));
}

/* On a phone the rule is what matters and the row has too many passengers.
   The spec takes a line of its own; the comment, the locked badge and the
   actions share the next. */
@media (max-width: 720px) {
	.rule-row {
		flex-wrap: wrap;
		row-gap: 6px;
	}

	/* Reordering is a drag and there is no touch drag implementation, so on a
	   phone the handle is dead weight that only steals width. */
	.rule-row .drag-handle {
		display: none;
	}

	.rule-spec {
		flex: 1 1 100%;
	}

	.rule-comment {
		flex: 1 1 auto;
		white-space: normal;
	}
}

/* ==========================================================================
   PHASE 29 — CLICK-TO-CALL: THE CALL BAR, THE NUMBER CHOOSER, THE SCREEN-POP
   ========================================================================== */

/* .btn-sm was used by the notifications dropdown from phase 11 and never
   defined, so that button has been rendering at full size ever since. Defining
   it here rather than inventing a second name fixes both places at once. */
.btn-sm {
	padding: 4px 9px;
	font-size: 11.5px;
	gap: 5px;
}

.btn-sm .icon {
	width: 13px;
	height: 13px;
}

/* --------------------------------------------------------------- the bar --

   A strip in the shell, between the topbar and the scrolling content. `.main`
   is a flex column, so flex-shrink:0 is what keeps it a strip rather than
   letting it absorb the page's growth.

   The three states are colour-coded rather than only worded, because an agent
   glances at this while talking: amber means somebody is waiting for you (your
   own phone ringing, or a caller ringing in), green means a live conversation,
   and the idle grey is deliberately almost invisible. */
.callbar {
	flex-shrink: 0;
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 7px 20px;
	min-height: 38px;
	font-size: 12.5px;
	background: var(--surface-2);
	border-bottom: 1px solid var(--border);
	color: var(--text-2);
}

.callbar.is-idle {
	color: var(--text-3);
}

.callbar.is-pending {
	background: var(--amber-soft);
	border-bottom-color: var(--amber);
	color: var(--text);
}

.callbar.is-live {
	background: var(--green-soft);
	border-bottom-color: var(--green);
	color: var(--text);
}

.callbar.is-live[data-state="ringing"] {
	background: var(--amber-soft);
	border-bottom-color: var(--amber);
}

.callbar-icon {
	display: inline-flex;
	flex-shrink: 0;
}

.callbar-state {
	font-weight: 600;
	white-space: nowrap;
}

/* The one part allowed to be squeezed: a long company name yields before the
   duration and the hangup button, which must never move or wrap. */
.callbar-who {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.callbar-num {
	color: var(--text-2);
	margin-left: 6px;
}

.callbar-timer {
	margin-left: auto;
	font-variant-numeric: tabular-nums;
	font-weight: 600;
	flex-shrink: 0;
}

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

.callbar-act {
	flex-shrink: 0;
}

/* Under 820px the strip loses the words it can afford to lose. The duration and
   the hangup button survive at every width, because they are the two things an
   agent on a call actually needs. */
@media (max-width: 820px) {
	.callbar {
		padding: 6px 14px;
		gap: 8px;
	}

	.callbar-state {
		display: none;
	}
}

/* ------------------------------------------------- bulk import into a pool --

   A <details> rather than a modal: a textarea and a file input are not a
   conversation, and a dialog is one more thing to open and dismiss for a
   control used twice a year. Shut by default, because the everyday act on this
   screen is adding one number. */
.pool-bulk {
	margin-top: 14px;
	border-top: 1px solid var(--border);
	padding-top: 12px;
}

.pool-bulk > summary {
	cursor: pointer;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	font-size: 12.5px;
	font-weight: 600;
	color: var(--text-2);
	list-style: none;
}

.pool-bulk > summary:hover {
	color: var(--text);
}

.pool-bulk > summary::-webkit-details-marker {
	display: none;
}

.pool-bulk-body {
	margin-top: 12px;
	/* The forms are full width at every size — a textarea in a half-width
	   column is a textarea nobody can read their paste in. */
	max-width: 640px;
}

.pool-bulk textarea {
	width: 100%;
	min-height: 120px;
	font-family: var(--mono, ui-monospace, monospace);
	font-size: 12.5px;
	line-height: 1.5;
	resize: vertical;
}

/* The refusals, quoted back with their source line. Scrolls rather than growing
   without bound: a 300-line paste with 200 bad rows must not push the pool it
   is reporting on off the screen. */
.import-skipped {
	margin: 8px 0 0;
	padding-left: 18px;
	max-height: 220px;
	overflow-y: auto;
	font-size: 12.5px;
	line-height: 1.6;
}

.import-skipped .mono {
	color: var(--text-2);
}

/* ----------------------------------------------------------- the Call panel --

   The <details> number chooser that used to live here went with the header Call
   button it hung off: the panel has room for a real <select>, and a dropdown
   inside a 260px sidebar column is a menu that leaves the viewport.

   Almost everything here is already the app's: `.field`, `.select`, `.hint` and
   `.btn` come from the form layer and answer 390px for free. What is left is the
   two things a sidebar column needs — the button spanning the column instead of
   hugging its text, and the single-number case looking like the value it is
   rather than like an input somebody failed to fill in. */
.call-panel [data-call-panel-go] {
	width: 100%;
	justify-content: center;
}

/* A <label> that labels no control would be a lie to a screen reader — the one
   number is text, not a field — so it is styled to match one instead. */
.call-panel .label-like {
	display: block;
	margin-bottom: 5px;
	font-size: 12px;
	font-weight: 600;
	color: var(--text);
}

.call-panel-one {
	padding: 7px 10px;
	background: var(--surface-2);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	font-size: 13px;
}

/* A disabled Call button must read as "not yet", not as broken. Dimmed and
   not-allowed, which is what every other disabled control in the app does. */
.call-panel [data-call-panel-go]:disabled {
	opacity: .55;
	cursor: not-allowed;
}

/* ---------------------------------------------------------- the screen-pop --

   Anchored under the bar rather than floating over the page: an agent answering
   a call is reading the record behind it, and a panel that covers the thing
   they are looking at is worse than one that pushes it down. */
.callpop-slot:empty {
	display: none;
}

.callpop {
	border-bottom: 1px solid var(--border);
	background: var(--surface);
	padding: 10px 20px 12px;
}

.callpop-head {
	display: flex;
	align-items: center;
	gap: 8px;
	font-size: 13px;
}

.callpop-dir {
	color: var(--text-3);
	font-size: 12px;
}

.callpop-list {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	margin-top: 8px;
}

.callpop-list li {
	display: flex;
	align-items: center;
	gap: 7px;
	padding: 5px 9px;
	border: 1px solid var(--border);
	border-radius: 8px;
	font-size: 12.5px;
}

.callpop-none {
	margin: 8px 0;
	color: var(--text-2);
	font-size: 12.5px;
}

/* ==========================================================================
   PHASE 30 — CALL HISTORY: THE CDR TABLE, THE ARI LOG, RECORDINGS
   ========================================================================== */

/* A CDR row has eight columns and one of them is a timestamp, so it is the one
   table in the application that genuinely cannot narrow to a phone. It scrolls
   sideways inside its own box rather than pushing the page wide — the rule the
   whole app follows: the body never scrolls horizontally, a wide thing does. */
.table-scroll {
	overflow-x: auto;
	-webkit-overflow-scrolling: touch;
}

.nowrap {
	white-space: nowrap;
}

.card-head-meta {
	margin-left: auto;
	font-size: 12px;
	color: var(--text-3);
}

/* -------------------------------------------------------------- the ARI log --

   A collapsed list of what the switch said. Closed by default because the
   summary line answers most questions and the payload is forty lines of JSON;
   open on demand because when the summary does NOT answer it, nothing else
   will. */
.ari-log {
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.ari-log summary {
	display: flex;
	align-items: baseline;
	gap: 8px;
	padding: 6px 8px;
	border-radius: 7px;
	cursor: pointer;
	font-size: 12.5px;
}

.ari-log summary:hover {
	background: var(--surface-2);
}

.ari-seq {
	min-width: 24px;
	color: var(--text-3);
	font-size: 11px;
}

.ari-at {
	margin-left: auto;
	color: var(--text-3);
	font-size: 11px;
	flex-shrink: 0;
}

.ari-json {
	margin: 2px 0 8px 32px;
	padding: 10px 12px;
	background: var(--surface-2);
	border: 1px solid var(--border);
	border-radius: 8px;
	font-family: var(--mono);
	font-size: 11.5px;
	line-height: 1.5;
	/* The payload is machine output of unbounded width. It scrolls in its own
	   box; it does not stretch the page. */
	overflow-x: auto;
	white-space: pre;
}

/* ------------------------------------------------------------- recordings -- */
.callrec-player {
	width: 100%;
	max-width: 100%;
	margin-bottom: 6px;
}

.callrec-note {
	font-size: 11.5px;
}

/* --------------------------------------------- the per-entity call history -- */
.entity-calls {
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.entity-calls > li {
	display: flex;
	flex-direction: column;
	gap: 4px;
	padding: 6px 0;
	border-bottom: 1px solid var(--border);
}

.entity-calls > li:last-child {
	border-bottom: 0;
}

.entity-call {
	display: flex;
	align-items: center;
	gap: 9px;
	min-width: 0;
	color: var(--text);
	font-size: 12.5px;
}

.entity-call:hover {
	color: var(--primary);
}

.entity-call-dir {
	display: inline-flex;
	flex-shrink: 0;
	color: var(--text-3);
}

.entity-call-when {
	display: flex;
	flex-direction: column;
	gap: 1px;
	min-width: 0;
}

.entity-call-when .muted {
	font-size: 11.5px;
}

/* A sidebar is 280-ish px, and the default audio control is wider than that in
   some browsers. Capping it keeps the panel from being the one thing on the
   page that overflows. */
.entity-call-audio {
	width: 100%;
	max-width: 100%;
	height: 30px;
}

.entity-call-dl {
	align-self: flex-start;
	color: var(--text-3);
}

.side-more {
	margin-top: 8px;
	font-size: 12px;
}

.side-more a {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	color: var(--primary);
	font-weight: 600;
}

/* ============================================================================
   PHASE 31 — SUPERVISION AND THE WALLBOARD
   ============================================================================ */

/* ------------------------------------------------ the supervisor's strip --

   Shaped like the call bar because it is the same kind of thing: a persistent
   strip about a live conversation. It is NOT in the shell zone, though — it
   belongs to the wallboard, and a supervision session that outlived the page
   you started it from would be a session you could forget you were running. */
.supbar {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 8px 16px;
	margin-bottom: 16px;
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface);
	font-size: 13px;
}

.supbar.is-live {
	border-color: var(--primary);
	background: var(--primary-soft);
}

.supbar-icon {
	display: inline-flex;
	color: var(--text-3);
	flex-shrink: 0;
}

.supbar.is-live .supbar-icon {
	color: var(--primary);
}

.supbar-state {
	font-weight: 600;
	flex-shrink: 0;
}

/* THE CONSEQUENCE, in words, always visible while a session is live. Not a
   tooltip: "only the agent hears you" and "both the agent and the customer hear
   you" is the difference the operator must never have to hover to learn. */
.supbar-audible {
	color: var(--text-2);
	font-size: 12px;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.supbar-who {
	display: flex;
	align-items: baseline;
	gap: 8px;
	min-width: 0;
}

.supbar-ext,
.supbar-text {
	color: var(--text-2);
	font-size: 12px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.supbar-timer {
	margin-left: auto;
	font-variant-numeric: tabular-nums;
	font-weight: 600;
	flex-shrink: 0;
}

.supbar-modes {
	display: flex;
	gap: 4px;
	flex-shrink: 0;
}

.supbar-act {
	flex-shrink: 0;
}

/* ------------------------------------------------------------ the board -- */

.wallboard-kpis {
	margin-bottom: 14px;
}

.wallboard-calls,
.wallboard-seats {
	margin-bottom: 16px;
}

.wallboard-rows {
	display: flex;
	flex-direction: column;
}

/* A row rather than a table cell: the board is read from across a room, and a
   flex row can drop the columns that do not survive a narrow screen without the
   header lying about what is in them. */
.wallrow {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 10px 16px;
	border-top: 1px solid var(--border);
	font-size: 13.5px;
}

.wallrow:first-child {
	border-top: none;
}

/* A monitored call is marked on the board for EVERYBODY who can see it, not
   only for supervisors. Somebody listening to a conversation is a fact about
   that conversation. */
.wallrow.is-monitored {
	background: var(--primary-soft);
}

.wallrow-who {
	display: flex;
	flex-direction: column;
	min-width: 0;
	flex: 1 1 auto;
}

.wallrow-who b {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.wallrow-who .mono {
	color: var(--text-2);
	font-size: 12px;
}

.wallrow-agent {
	flex: 0 0 auto;
	color: var(--text-2);
	max-width: 22ch;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.wallrow-sup:empty {
	display: none;
}

.wallrow-acts {
	display: flex;
	gap: 4px;
	flex-shrink: 0;
}

.wallrow-timer {
	flex: 0 0 auto;
	min-width: 5ch;
	text-align: right;
	font-variant-numeric: tabular-nums;
	font-weight: 600;
}

/* -------------------------------------------------------------- the seats -- */

.seat-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
	gap: 10px;
	padding: 14px 16px;
}

.seat {
	padding: 10px 12px;
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface);
}

.seat.is-on-call {
	border-color: var(--primary);
}

.seat-name {
	display: flex;
	align-items: center;
	gap: 6px;
	font-weight: 600;
	font-size: 13px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.seat-meta {
	display: flex;
	align-items: center;
	gap: 8px;
	margin-top: 6px;
	font-size: 11.5px;
	color: var(--text-2);
	min-width: 0;
}

.seat-party,
.seat-ext {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* Under 820px the board drops what it can afford to. The state badge, the other
   party and the duration survive at every width, because those three are what a
   wallboard is for; the agent column and the supervise buttons wrap below. */
@media (max-width: 820px) {
	.wallboard-kpis {
		grid-template-columns: repeat(2, 1fr);
	}

	.wallrow {
		flex-wrap: wrap;
		gap: 8px;
	}

	.wallrow-agent {
		max-width: 100%;
	}

	.wallrow-timer {
		margin-left: auto;
	}

	.supbar {
		flex-wrap: wrap;
		gap: 8px;
	}

	.supbar-audible {
		flex-basis: 100%;
		order: 10;
	}

	.supbar-timer {
		margin-left: 0;
	}
}

/* The agent-awareness indicator on the call bar. Present only when the operator
   has switched `notify_monitored` on and somebody is actually listening — with
   the setting off there is no element here at all, hidden or otherwise. */
.callbar-monitored {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	flex-shrink: 0;
	padding: 2px 8px;
	border-radius: 999px;
	background: var(--primary-soft);
	color: var(--primary);
	font-size: 11.5px;
	font-weight: 600;
	white-space: nowrap;
}

@media (max-width: 820px) {
	/* The words go; the icon stays. An agent being coached mid-call needs to
	   know it is happening on a phone screen too. */
	.callbar-monitored {
		font-size: 0;
		gap: 0;
		padding: 2px 6px;
	}
}

/* ---------- Tickets: the conversation ---------- */

/* The original request, on the details card. `pre-wrap` keeps the line breaks
   somebody typed; `anywhere` stops a pasted URL widening the whole card. */
.ticket-original {
	margin: 4px 0 0;
	font-size: 13px;
	line-height: 1.55;
	white-space: pre-wrap;
	overflow-wrap: anywhere;
}

.ticket-thread {
	display: grid;
	gap: 10px;
	padding: 14px 18px;
}

.ticket-msg {
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface);
	overflow: hidden;
}

/* An internal note is marked three ways at once — this frame, the padlock and
   the words. The cost of mistaking one for a reply is a note about a customer
   being sent to that customer, so one cue is not enough. */
.ticket-msg.is-internal {
	border-color: var(--amber);
	background: var(--amber-soft);
}

.ticket-msg-head {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	padding: 8px 12px;
	border-bottom: 1px solid var(--border);
	font-size: 12px;
}

.ticket-msg.is-internal .ticket-msg-head {
	border-bottom-color: rgba(217, 119, 6, .3);
}

.ticket-msg-head .who {
	font-weight: 600;
}

.ticket-msg-body {
	margin: 0;
	padding: 10px 12px;
	font-size: 13px;
	line-height: 1.55;
	white-space: pre-wrap;
	overflow-wrap: anywhere;
}

.ticket-compose {
	display: grid;
	gap: 8px;
}

.ticket-compose-modes {
	display: flex;
	gap: 14px;
	flex-wrap: wrap;
}

/* The composer takes the colour of what it is about to write, so the mode is
   visible while the text is being typed rather than only where it was chosen. */
.ticket-compose.is-internal textarea {
	border-color: var(--amber);
	background: var(--amber-soft);
}

.ticket-compose-foot {
	display: flex;
	align-items: center;
	gap: 10px;
	flex-wrap: wrap;
}

.ticket-compose-foot .hint {
	margin: 0;
	margin-right: auto;
}

@media (max-width: 820px) {
	.ticket-thread {
		padding: 12px 14px;
	}

	/* The button goes full width rather than sitting beside a hint that has
	   wrapped to two lines above it. */
	.ticket-compose-foot .btn {
		width: 100%;
		justify-content: center;
	}
}

/* The requester pair, which is one form rather than two — `.lead-controls form`
   is a one-column grid, right for the single-control forms beside it and wrong
   for a form that holds a type, an id and a button. */
.ticket-requester-form {
	display: flex;
	align-items: flex-end;
	gap: 12px;
	flex-wrap: wrap;
}

.ticket-requester-form .ctl {
	display: grid;
	gap: 4px;
}

@media (max-width: 820px) {
	/* Stacked, and the button spans — a 90px "Save" floating beside a wrapped
	   number box reads as belonging to the box rather than to the pair. */
	.ticket-requester-form .ctl,
	.ticket-requester-form .btn {
		width: 100%;
	}

	.ticket-requester-form .btn {
		justify-content: center;
	}
}

/* ============================================================================
   SPACING UTILITIES
   ============================================================================

   Margin, padding and gap on the scale in tokens.css. Bootstrap's names,
   because everybody already knows them; this app's values, because a second
   spacing system is a second spacing system.

       {m|p}{side}-{step}      m-3   mt-2   px-4   mb-0   my-3
       gap-{step}              gap-2
       m{side}-auto            mx-auto   ms-auto

   SIDES   (none) all four   t top      b bottom
           x left+right      y top+bottom
           s start / e end   — the logical pair, which is what these really are
           l left  / r right — plain aliases, for when "start" reads as jargon

   STEPS   0 → 0     1 → 4px    2 → 8px
           3 → 12px  4 → 16px   5 → 24px

   ---------------------------------------------------------------------------
   WHY !important
   ---------------------------------------------------------------------------

   Because a utility that sometimes loses is worse than no utility. Nearly every
   component rule in this file is a descendant selector — `.field .hint`,
   `.card-head .badge` — and at (0,2,0) it outranks a single class. Without
   `!important` a `.mt-3` on a hint would silently do nothing, and the failure
   is invisible: the markup says the spacing is there. Bootstrap made the same
   call for the same reason.

   The cost is that a component cannot override a utility, which is the right
   way round: the utility is the more specific INTENT, written on the element by
   the person looking at the screen.

   ---------------------------------------------------------------------------
   WHAT IS DELIBERATELY MISSING
   ---------------------------------------------------------------------------

   No negative margins, no responsive variants (`md:mt-3`), no per-breakpoint
   scale. Each would multiply this block several times over to serve cases the
   app does not have — every screen here is one column at 390px and the same
   column wider, and a layout that needs to change its spacing at a breakpoint
   is a component, not a utility. Add them when something actually needs them.
   ========================================================================= */

/* ---------- margin ---------- */

.m-0 {
	margin: var(--space-0) !important;
}

.mt-0 {
	margin-top: var(--space-0) !important;
}

.mb-0 {
	margin-bottom: var(--space-0) !important;
}

.ms-0,
.ml-0 {
	margin-inline-start: var(--space-0) !important;
}

.me-0,
.mr-0 {
	margin-inline-end: var(--space-0) !important;
}

.mx-0 {
	margin-inline-start: var(--space-0) !important;
	margin-inline-end: var(--space-0) !important;
}

.my-0 {
	margin-top: var(--space-0) !important;
	margin-bottom: var(--space-0) !important;
}

.m-1 {
	margin: var(--space-1) !important;
}

.mt-1 {
	margin-top: var(--space-1) !important;
}

.mb-1 {
	margin-bottom: var(--space-1) !important;
}

.ms-1,
.ml-1 {
	margin-inline-start: var(--space-1) !important;
}

.me-1,
.mr-1 {
	margin-inline-end: var(--space-1) !important;
}

.mx-1 {
	margin-inline-start: var(--space-1) !important;
	margin-inline-end: var(--space-1) !important;
}

.my-1 {
	margin-top: var(--space-1) !important;
	margin-bottom: var(--space-1) !important;
}

.m-2 {
	margin: var(--space-2) !important;
}

.mt-2 {
	margin-top: var(--space-2) !important;
}

.mb-2 {
	margin-bottom: var(--space-2) !important;
}

.ms-2,
.ml-2 {
	margin-inline-start: var(--space-2) !important;
}

.me-2,
.mr-2 {
	margin-inline-end: var(--space-2) !important;
}

.mx-2 {
	margin-inline-start: var(--space-2) !important;
	margin-inline-end: var(--space-2) !important;
}

.my-2 {
	margin-top: var(--space-2) !important;
	margin-bottom: var(--space-2) !important;
}

.m-3 {
	margin: var(--space-3) !important;
}

.mt-3 {
	margin-top: var(--space-3) !important;
}

.mb-3 {
	margin-bottom: var(--space-3) !important;
}

.ms-3,
.ml-3 {
	margin-inline-start: var(--space-3) !important;
}

.me-3,
.mr-3 {
	margin-inline-end: var(--space-3) !important;
}

.mx-3 {
	margin-inline-start: var(--space-3) !important;
	margin-inline-end: var(--space-3) !important;
}

.my-3 {
	margin-top: var(--space-3) !important;
	margin-bottom: var(--space-3) !important;
}

.m-4 {
	margin: var(--space-4) !important;
}

.mt-4 {
	margin-top: var(--space-4) !important;
}

.mb-4 {
	margin-bottom: var(--space-4) !important;
}

.ms-4,
.ml-4 {
	margin-inline-start: var(--space-4) !important;
}

.me-4,
.mr-4 {
	margin-inline-end: var(--space-4) !important;
}

.mx-4 {
	margin-inline-start: var(--space-4) !important;
	margin-inline-end: var(--space-4) !important;
}

.my-4 {
	margin-top: var(--space-4) !important;
	margin-bottom: var(--space-4) !important;
}

.m-5 {
	margin: var(--space-5) !important;
}

.mt-5 {
	margin-top: var(--space-5) !important;
}

.mb-5 {
	margin-bottom: var(--space-5) !important;
}

.ms-5,
.ml-5 {
	margin-inline-start: var(--space-5) !important;
}

.me-5,
.mr-5 {
	margin-inline-end: var(--space-5) !important;
}

.mx-5 {
	margin-inline-start: var(--space-5) !important;
	margin-inline-end: var(--space-5) !important;
}

.my-5 {
	margin-top: var(--space-5) !important;
	margin-bottom: var(--space-5) !important;
}

/* ---------- padding ---------- */

.p-0 {
	padding: var(--space-0) !important;
}

.pt-0 {
	padding-top: var(--space-0) !important;
}

.pb-0 {
	padding-bottom: var(--space-0) !important;
}

.ps-0,
.pl-0 {
	padding-inline-start: var(--space-0) !important;
}

.pe-0,
.pr-0 {
	padding-inline-end: var(--space-0) !important;
}

.px-0 {
	padding-inline-start: var(--space-0) !important;
	padding-inline-end: var(--space-0) !important;
}

.py-0 {
	padding-top: var(--space-0) !important;
	padding-bottom: var(--space-0) !important;
}

.p-1 {
	padding: var(--space-1) !important;
}

.pt-1 {
	padding-top: var(--space-1) !important;
}

.pb-1 {
	padding-bottom: var(--space-1) !important;
}

.ps-1,
.pl-1 {
	padding-inline-start: var(--space-1) !important;
}

.pe-1,
.pr-1 {
	padding-inline-end: var(--space-1) !important;
}

.px-1 {
	padding-inline-start: var(--space-1) !important;
	padding-inline-end: var(--space-1) !important;
}

.py-1 {
	padding-top: var(--space-1) !important;
	padding-bottom: var(--space-1) !important;
}

.p-2 {
	padding: var(--space-2) !important;
}

.pt-2 {
	padding-top: var(--space-2) !important;
}

.pb-2 {
	padding-bottom: var(--space-2) !important;
}

.ps-2,
.pl-2 {
	padding-inline-start: var(--space-2) !important;
}

.pe-2,
.pr-2 {
	padding-inline-end: var(--space-2) !important;
}

.px-2 {
	padding-inline-start: var(--space-2) !important;
	padding-inline-end: var(--space-2) !important;
}

.py-2 {
	padding-top: var(--space-2) !important;
	padding-bottom: var(--space-2) !important;
}

.p-3 {
	padding: var(--space-3) !important;
}

.pt-3 {
	padding-top: var(--space-3) !important;
}

.pb-3 {
	padding-bottom: var(--space-3) !important;
}

.ps-3,
.pl-3 {
	padding-inline-start: var(--space-3) !important;
}

.pe-3,
.pr-3 {
	padding-inline-end: var(--space-3) !important;
}

.px-3 {
	padding-inline-start: var(--space-3) !important;
	padding-inline-end: var(--space-3) !important;
}

.py-3 {
	padding-top: var(--space-3) !important;
	padding-bottom: var(--space-3) !important;
}

.p-4 {
	padding: var(--space-4) !important;
}

.pt-4 {
	padding-top: var(--space-4) !important;
}

.pb-4 {
	padding-bottom: var(--space-4) !important;
}

.ps-4,
.pl-4 {
	padding-inline-start: var(--space-4) !important;
}

.pe-4,
.pr-4 {
	padding-inline-end: var(--space-4) !important;
}

.px-4 {
	padding-inline-start: var(--space-4) !important;
	padding-inline-end: var(--space-4) !important;
}

.py-4 {
	padding-top: var(--space-4) !important;
	padding-bottom: var(--space-4) !important;
}

.p-5 {
	padding: var(--space-5) !important;
}

.pt-5 {
	padding-top: var(--space-5) !important;
}

.pb-5 {
	padding-bottom: var(--space-5) !important;
}

.ps-5,
.pl-5 {
	padding-inline-start: var(--space-5) !important;
}

.pe-5,
.pr-5 {
	padding-inline-end: var(--space-5) !important;
}

.px-5 {
	padding-inline-start: var(--space-5) !important;
	padding-inline-end: var(--space-5) !important;
}

.py-5 {
	padding-top: var(--space-5) !important;
	padding-bottom: var(--space-5) !important;
}

/* ---------- auto margins ----------

   Margins only: `padding: auto` is not a thing. `mx-auto` centres a block that
   has a width, which is the one this app reaches for; `ms-auto` pushes an item
   to the end of a flex row, which several card heads currently do with an
   inline style. */

.m-auto {
	margin: auto !important;
}

.mt-auto {
	margin-top: auto !important;
}

.mb-auto {
	margin-bottom: auto !important;
}

.ms-auto,
.ml-auto {
	margin-inline-start: auto !important;
}

.me-auto,
.mr-auto {
	margin-inline-end: auto !important;
}

.mx-auto {
	margin-inline-start: auto !important;
	margin-inline-end: auto !important;
}

.my-auto {
	margin-top: auto !important;
	margin-bottom: auto !important;
}

/* ---------- gap ----------

   Worth having: `gap` appears over 150 times in this file, and a row of buttons
   or a small grid is exactly the kind of thing a page assembles rather than a
   component owns. Row and column variants are included because a grid that
   wants tight columns and loose rows is common enough to be worth two lines
   each; anything finer belongs in the component. */

.gap-0 {
	gap: var(--space-0) !important;
}

.gap-x-0 {
	column-gap: var(--space-0) !important;
}

.gap-y-0 {
	row-gap: var(--space-0) !important;
}

.gap-1 {
	gap: var(--space-1) !important;
}

.gap-x-1 {
	column-gap: var(--space-1) !important;
}

.gap-y-1 {
	row-gap: var(--space-1) !important;
}

.gap-2 {
	gap: var(--space-2) !important;
}

.gap-x-2 {
	column-gap: var(--space-2) !important;
}

.gap-y-2 {
	row-gap: var(--space-2) !important;
}

.gap-3 {
	gap: var(--space-3) !important;
}

.gap-x-3 {
	column-gap: var(--space-3) !important;
}

.gap-y-3 {
	row-gap: var(--space-3) !important;
}

.gap-4 {
	gap: var(--space-4) !important;
}

.gap-x-4 {
	column-gap: var(--space-4) !important;
}

.gap-y-4 {
	row-gap: var(--space-4) !important;
}

.gap-5 {
	gap: var(--space-5) !important;
}

.gap-x-5 {
	column-gap: var(--space-5) !important;
}

.gap-y-5 {
	row-gap: var(--space-5) !important;
}

/* ---------- the styleguide's spacing demo ----------

   Only the /styleguide page uses these. They live here rather than inline on
   that page because the spacing section exists to say "stop writing inline
   styles", and a reference that breaks its own rule is not one. */

.spacing-scale {
	display: flex;
	flex-wrap: wrap;
	gap: 20px;
	align-items: flex-end;
}

.spacing-swatch {
	height: 26px;
	min-width: 2px;
	margin-bottom: 6px;
	background: var(--primary-soft);
	border: 1px solid var(--primary-soft-border);
	border-radius: 4px;
}

.spacing-demo {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
	gap: 16px;
}

.spacing-box {
	background: var(--surface-2);
	border: 1px dashed var(--border-strong);
	border-radius: var(--radius);
	font-size: 12.5px;
	color: var(--text-2);
	padding: 8px;
}
/* ---------- the detail sidebar's cards ----------

   The cards themselves are `.card` and nothing else — same chrome as the
   details, custom-fields and activity blocks in the left column, which is the
   point of the split. What is left here is the column that stacks them and the
   drag affordance an admin gets.

   `.sidebar-widget` (the lazily-fetched body inside each card) keeps whatever
   it already had; it is still a `.card-body`. */

[data-sidebar-cards] {
	display: flex;
	flex-direction: column;
	gap: var(--space-4);
}

/* The gap above owns the spacing between cards, so a card must not also carry
   the bottom margin `.card` has for the left column's flow layout. */
[data-sidebar-cards] > .card {
	margin: 0;
}

/* The grip sits before the title, so the head is a row that STARTS with it.

   `.card-head` is `justify-content: space-between`, which is right for the left
   column — a title at one end, a badge or an Edit link at the other. Here the
   grip is a second child with nothing opposite it, so space-between threw the
   title to the right edge and an admin saw right-aligned titles while everyone
   else saw left-aligned ones. Two children, both at the start. */
.sidebar-card > .card-head {
	justify-content: flex-start;
	gap: 8px;
}

/* `.drag-handle` is the app's existing handle — same cursor, same colour, same
   hover as the menu builder and the pipeline admin. */
.sidebar-card > .card-head > .drag-handle {
	cursor: grab;
}

.sidebar-card[draggable="true"] > .card-head {
	cursor: grab;
}

.sidebar-card.is-dragging {
	opacity: .5;
}

/* VERTICAL ONLY, and the markers say so: a line above or below, never beside.
   Same treatment as `.stage-row` and `.menu-row`, which are also single-axis
   lists. */
.sidebar-card.drop-before {
	box-shadow: 0 -3px 0 -1px var(--primary);
}

.sidebar-card.drop-after {
	box-shadow: 0 3px 0 -1px var(--primary);
}

/* On a phone the sidebar stacks under the content and the cards are full
   width. The drag still works — HTML5 DnD is pointer-driven and touch support
   varies by browser — but the order is the saved one either way, so a device
   that cannot drag loses nothing it can see. */
@media (max-width: 900px) {
	[data-sidebar-cards] {
		gap: var(--space-3);
	}
}

/* ---------- Global search ----------

   Two surfaces, one look: the palette overlay's grouped rows and the /search
   page's result lists. The palette's shell — .cmdk, .cmdk-card, .cmdk-input,
   .cmdk-item — is untouched from before phase 47; what is new is the grouping
   and the two-line row, because a record needs a secondary line and a page
   link never did. */

.cmdk-group + .cmdk-group {
	margin-top: var(--space-2);
	border-top: 1px solid var(--border);
	padding-top: var(--space-2);
}

.cmdk-group-head {
	padding: 6px 11px;
	font-size: 10.5px;
	font-weight: 700;
	letter-spacing: .6px;
	text-transform: uppercase;
	color: var(--text-3);
}

/* The row is an <a> now rather than a <button>, so it needs the colour and the
   underline reset that the button never did. */
.cmdk-item {
	color: inherit;
	text-decoration: none;
}

.cmdk-item-text {
	display: flex;
	flex-direction: column;
	gap: 1px;
	min-width: 0;
}

.cmdk-item-label,
.cmdk-item-hint {
	display: block;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.cmdk-item-hint {
	font-size: 11.5px;
	color: var(--text-3);
}

/* Grouped, two-line rows need more room than the flat one-line list this
   started as; 320px cut the fifth section off mid-heading. */
.cmdk-list {
	max-height: min(60vh, 440px);
}

/* "See all results" closes the list, so it reads as a footer rather than as
   one more record. */
.cmdk-all {
	margin-top: var(--space-2);
	border-top: 1px solid var(--border);
	border-radius: 0 0 8px 8px;
	color: var(--primary);
	font-weight: 600;
}

.cmdk-all .icon {
	margin-left: auto;
	color: var(--primary);
}

/* The tab strip on /search is the app's existing one; it was written for
   <button> and these are links, because a tab here is a shareable URL. */
.tabs a.tab {
	padding: 10px 12px;
	font-size: 12.5px;
	font-weight: 600;
	color: var(--text-3);
	border-bottom: 2px solid transparent;
	margin-bottom: -1px;
	text-decoration: none;
}

.tabs a.tab.on {
	color: var(--primary);
	border-color: var(--primary);
}

.tab-count {
	font-weight: 600;
	color: var(--text-3);
}

.search-bar {
	display: flex;
	align-items: center;
}

.search-bar input {
	flex: 1;
	min-width: 0;
}

.search-results {
	list-style: none;
}

.search-results li + li {
	border-top: 1px solid var(--border);
}

.search-result {
	display: flex;
	flex-direction: column;
	gap: 2px;
	padding: 11px 18px;
	color: inherit;
	text-decoration: none;
}

.search-result:hover {
	background: var(--primary-soft);
}

.search-result-label {
	font-size: 13.5px;
	font-weight: 600;
}

.search-result-hint {
	font-size: 12px;
	color: var(--text-3);
}

/* On a phone the palette is nearly the whole screen and the topbar's trigger
   collapses to its icon, so the overlay has to be reachable and the rows have
   to fit without pushing the card sideways. */
@media (max-width: 820px) {
	.cmdk-card {
		margin-top: 6vh;
	}

	.cmdk-list {
		max-height: 60vh;
	}

	.tabs {
		overflow-x: auto;
		padding-inline: 0;
	}
}

/* ---------- Teams ----------

   A team is a name, a colour and some people, so the whole of this is a swatch
   and two list layouts. Everything else — cards, badges, buttons, fields — is
   the app's existing chrome. */

/* A swatch, sized like one. The `.field` rule above excludes [type=color]
   from the fill-the-column treatment so this is reachable at all. */
input[type="color"] {
	width: 64px;
	height: 34px;
	padding: 3px;
	cursor: pointer;
}

.team-swatch {
	width: 10px;
	height: 10px;
	border-radius: 3px;
	flex: none;
	box-shadow: inset 0 0 0 1px rgb(0 0 0 / .12);
}

.team-name {
	display: flex;
	align-items: center;
	gap: 10px;
	color: inherit;
	text-decoration: none;
}

.muted-line {
	display: block;
	font-size: 12px;
	color: var(--text-3);
}

/* The add-member control: a select that takes the room, a button that does not. */
.add-member-row {
	display: flex;
	align-items: center;
}

.add-member-row select {
	flex: 1;
	min-width: 0;
}

.member-list {
	list-style: none;
}

.member-row {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 9px 18px;
	border-top: 1px solid var(--border);
}

.member-who {
	display: flex;
	flex-direction: column;
	gap: 1px;
	min-width: 0;
	/* Takes the slack so the actions sit hard right whatever the name's
	   length, and truncates rather than pushing them off the card. */
	flex: 1;
	overflow: hidden;
}

.member-who a {
	font-size: 13.5px;
	font-weight: 600;
	color: inherit;
	text-decoration: none;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.member-actions {
	display: flex;
	align-items: center;
	gap: 4px;
	flex: none;
}

/* ---- the dashboard tile, and the teams strip on a profile ---- */

.team-tiles {
	list-style: none;
	display: grid;
	gap: var(--space-3);
}

.team-tile {
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: var(--space-3);
}

.team-tile-head {
	display: flex;
	align-items: center;
	gap: 8px;
	font-size: 13.5px;
}

.team-roster {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2);
	margin-top: var(--space-2);
}

.team-mate {
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: 12px;
	color: var(--text-2);
	background: var(--surface-2);
	border-radius: 999px;
	padding: 3px 10px 3px 3px;
}

/* A person's teams, read-only, on their profile and on their user page. */
.team-chips {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2);
}

.team-chip {
	display: flex;
	align-items: center;
	gap: 7px;
	font-size: 12.5px;
	border: 1px solid var(--border);
	border-radius: 999px;
	padding: 4px 11px;
}

.team-chip.is-inactive {
	color: var(--text-3);
	border-style: dashed;
}

/* On a phone the member row's actions wrap under the name rather than
   squeezing it to nothing. */
@media (max-width: 560px) {
	.member-row {
		flex-wrap: wrap;
	}

	.member-actions {
		margin-left: auto;
	}
}

/* A ticket's team, on the list and beside the assignee control. Small: it sits
   under a person's name, and the swatch is what makes it recognisable. */
.ticket-team-badge {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	font-size: 11.5px;
	color: var(--text-3);
}

/* ---------- Presence ----------

   The topbar pill, its dropdown, and the "also here" cue on a record. All of
   it is chrome the shell already has — a button, a card-shaped panel, the
   avatar component — plus one thing that is genuinely new: the status dot. */

.presence-dot {
	width: 8px;
	height: 8px;
	border-radius: 50%;
	flex: none;
	background: var(--text-3);
}

.presence-dot.is-online {
	background: var(--green);
}

.presence-dot.is-idle {
	background: var(--amber);
}

.presence-dot.is-offline {
	background: var(--text-3);
}

/* The panel is absolutely positioned against this, not against the topbar:
   `.topbar-action` is not a positioned ancestor, so without this the dropdown
   would anchor to the page and land somewhere else entirely. */
#presence-indicator {
	position: relative;
	display: flex;
	align-items: center;
}

.presence-pill {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 5px 10px;
	border-radius: 999px;
	border: 1px solid var(--border);
	font-size: 12px;
	color: var(--text-2);
	background: var(--surface);
	cursor: pointer;
}

.presence-pill:hover {
	background: var(--surface-2);
}

.presence-count {
	font-weight: 600;
	color: var(--text);
}

.presence-panel {
	display: none;
	position: absolute;
	top: calc(100% + 6px);
	right: 0;
	width: 300px;
	max-height: 60vh;
	overflow-y: auto;
	z-index: 60;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: var(--shadow-lg);
}

.presence-panel.show {
	display: block;
}

.presence-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	padding: 10px 14px;
	border-bottom: 1px solid var(--border);
	font-size: 11px;
	font-weight: 700;
	letter-spacing: .5px;
	text-transform: uppercase;
	color: var(--text-3);
}

.presence-note {
	font-weight: 500;
	text-transform: none;
	letter-spacing: 0;
}

.presence-list {
	list-style: none;
	padding: 6px;
}

.presence-row {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 7px 8px;
	border-radius: 8px;
}

.presence-row.is-self {
	background: var(--surface-2);
}

/* The dot rides the avatar's corner, which is where somebody looks for it. */
.presence-avatar {
	position: relative;
	flex: none;
	display: flex;
}

.presence-avatar .presence-dot {
	position: absolute;
	right: -1px;
	bottom: -1px;
	box-shadow: 0 0 0 2px var(--surface);
}

.presence-who {
	display: flex;
	flex-direction: column;
	gap: 1px;
	min-width: 0;
}

.presence-name {
	font-size: 12.5px;
	font-weight: 600;
}

.presence-context,
.presence-context:visited {
	font-size: 11.5px;
	color: var(--text-3);
	text-decoration: none;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

a.presence-context:hover {
	color: var(--primary);
}

.presence-empty {
	padding: 20px 14px;
	text-align: center;
	font-size: 12.5px;
	color: var(--text-3);
}

/* ---- "also here", inline in a record's header actions ---- */

.also-here {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	padding: 4px 10px 4px 8px;
	border-radius: 999px;
	background: var(--amber-soft);
	color: var(--text-2);
	font-size: 12px;
}

/*
 * The cue lives on every record page and hides itself until somebody else is
 * actually there, so it has to be able to hide. `display: inline-flex` above
 * beats the user agent's `[hidden] { display: none }` — same specificity,
 * later sheet — which left an empty amber pill on every record in the app
 * until this line existed. Any class that sets `display` owes its `[hidden]`
 * the same courtesy.
 */
.also-here[hidden] {
	display: none;
}

.also-here .icon {
	color: var(--amber);
}

.also-here-faces {
	display: inline-flex;
}

/* Overlapped, the way a presence stack reads everywhere else. */
.also-here-face + .also-here-face {
	margin-left: -7px;
}

.also-here-face .avatar {
	box-shadow: 0 0 0 2px var(--surface);
}

@media (max-width: 820px) {
	/* The pill loses its word and keeps its number; the panel takes the width
	   it needs rather than hanging off the edge of a phone. */
	.presence-word {
		display: none;
	}

	.presence-panel {
		width: min(300px, calc(100vw - 24px));
		right: -8px;
	}

	/*
	 * The cue's sentence used to go at 820 because the cue was jammed into the
	 * row of header buttons and there was no room for it. Since phase 62 it has
	 * its own line under the record's name, so it keeps its words down to phone
	 * width and only sheds them where the line itself gets tight.
	 */
	.also-here-text {
		font-size: 11.5px;
	}
}

@media (max-width: 480px) {
	.also-here-text {
		display: none;
	}
}

/* ---- the inbound lead API, on the source list ---- */

/*
 * The API half of a source row, under the name it belongs to. Its own line
 * rather than more controls on the `.stage-row`: that row is already a name, a
 * count, a merge and a delete, and at 390px a fifth control turns it into a
 * column of buttons nobody can read.
 */
.source-api {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px;
	padding: 0 18px 11px 43px;
	font-size: 12.5px;
	color: var(--text-3);
}

.source-api form {
	display: inline-flex;
}

.api-secret-panel {
	margin: 0 18px 12px 43px;
	padding: 12px 14px;
	border: 1px solid var(--amber);
	border-radius: 10px;
	background: var(--amber-soft);
}

.api-secret-head {
	display: flex;
	align-items: center;
	gap: 8px;
	margin-bottom: 8px;
	color: var(--text-2);
}

/*
 * The key itself. `user-select: all` so one click selects the whole thing —
 * this is shown exactly once and a half-copied credential is a support ticket
 * that starts with a 401 nobody can explain.
 */
.api-secret {
	display: block;
	margin-bottom: 8px;
	padding: 8px 10px;
	border-radius: 8px;
	background: var(--surface);
	font-size: 14px;
	word-break: break-all;
	user-select: all;
}

@media (max-width: 820px) {
	/* The indent that lines these up under a source's name is a desktop
	   nicety; at this width it is 43px of nothing. */
	.source-api,
	.api-secret-panel {
		padding-left: 18px;
		margin-left: 0;
		margin-right: 0;
	}
}

/* ---- bulk selection on a list ---- */

/*
 * The bar sits between the filters and the table, in the flow rather than
 * floating over it: a fixed bar covers the last row on a short list and moves
 * under the thumb on a phone, and this one only exists while something is
 * selected — so it pushes the table down at the moment somebody is looking at
 * the top of it anyway.
 */
/*
 * `[hidden]` is only a UA rule of `display: none`, and any display declaration
 * in a stylesheet beats it — so a bar that says `display: flex` is a bar that
 * is always open, ticked rows or not. Every hideable flex component here needs
 * this line; this is the second one to have learned it the hard way.
 */
.bulk-bar[hidden] {
	display: none;
}

.bulk-bar {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px;
	padding: 10px 18px;
	border-top: 1px solid var(--border);
	background: var(--primary-soft);
}

.bulk-count {
	font-weight: 600;
	font-size: 13px;
	color: var(--text-1);
}

.bulk-sep {
	width: 1px;
	align-self: stretch;
	background: var(--border);
}

.bulk-form {
	display: inline-flex;
	align-items: center;
	gap: 6px;
}

/* Just wide enough for the checkbox, so the name column keeps the room. */
.col-select {
	width: 34px;
	padding-right: 0 !important;
}

.col-select input {
	margin: 0;
	cursor: pointer;
}

/* ---- the export page's column picker ---- */

.column-picker {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
	gap: 6px 14px;
}

.column-option {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 5px 0;
	font-size: 13px;
	cursor: pointer;
}

.column-option input {
	margin: 0;
}

.export-go {
	display: flex;
	align-items: center;
	justify-content: space-between;
	flex-wrap: wrap;
	gap: 12px;
}

@media (max-width: 820px) {
	/* Each action gets its own line rather than a row of half-controls. */
	.bulk-form {
		width: 100%;
	}

	.bulk-form .select {
		flex: 1;
	}

	.bulk-sep {
		display: none;
	}
}

/* ---------- Compose drawer (email) ----------
 *
 * The panel's own layout: a fixed header, a scrolling body, a fixed footer, so
 * Send is reachable without scrolling past a quoted thread. Everything else
 * reuses the app's fields, buttons and tokens. There is no third-party CSS in
 * here at all since phase 63 took the rich-text editor out.
 */

.compose {
	display: flex;
	flex-direction: column;
	height: 100%;
	min-height: 0;
}

.compose-head {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 12px;
	padding: 16px 18px;
	border-bottom: 1px solid var(--border);
}

.compose-title {
	font-weight: 600;
	font-size: 15px;
}

.compose-sub {
	font-size: 12px;
	color: var(--text-3);
	margin-top: 2px;
}

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

.compose-body {
	padding: 14px 18px;
	overflow-y: auto;
	flex: 1;
	min-height: 0;
}

.compose-row .field {
	margin-bottom: 10px;
}

.compose-templates {
	display: flex;
	gap: 8px;
	align-items: center;
	margin-bottom: 10px;
}

.compose-templates .select {
	flex: 1;
	min-width: 0;
}

.compose-warning {
	display: flex;
	align-items: flex-start;
	gap: 8px;
	padding: 8px 10px;
	margin-bottom: 10px;
	border-radius: 8px;
	background: var(--warning-soft, #fff6e5);
	color: var(--text-2);
	font-size: 12.5px;
}

/* ---------- The message box ----------
 *
 * A textarea, and a small bar of buttons that wrap the selection in a tag.
 * Phase 63 removed Quill and everything around it — the loader, the fallback,
 * the source toggle, the mirror — so what is styled here is the field itself.
 * See modules/email/templates/partials/editor.latte for why. */

.compose-editor {
	margin-top: 12px;
	border: 1px solid var(--border);
	border-radius: 10px;
	background: var(--surface);
	overflow: hidden;
}

.compose-editor.has-error {
	border-color: var(--red);
}

.compose-tools {
	display: flex;
	align-items: center;
	gap: 2px;
	padding: 5px 6px;
	border-bottom: 1px solid var(--border);
	background: var(--bg);
}

.compose-tool {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 28px;
	height: 26px;
	border-radius: 6px;
	color: var(--text-2);
	font-size: 13px;
	line-height: 1;
}

.compose-tool:hover {
	background: var(--surface-2);
	color: var(--text);
}

.compose-tools-note {
	margin-left: auto;
	font-family: var(--mono);
	font-size: 10.5px;
	letter-spacing: .04em;
	color: var(--text-3);
	text-transform: uppercase;
}

/* The field. Monospaced, because what it holds is prose WITH TAGS IN IT and a
   proportional face makes `<p>` disappear into the sentence around it. */
.compose-input {
	display: block;
	width: 100%;
	min-height: 220px;
	max-height: 46vh;
	padding: 11px 12px;
	border: 0;
	border-radius: 0;
	background: transparent;
	color: var(--text);
	/* The body font, not the mono one. It was monospace because the box held
	 * markup and a `<td>` is easier to read aligned; it holds a letter now, and
	 * a letter written in a terminal font reads as configuration. The three
	 * markdown markers do not need columns. */
	font: inherit;
	font-size: 14px;
	line-height: 1.65;
	resize: vertical;
}

.compose-input:focus {
	outline: 0;
	box-shadow: none;
}

.compose-editor-hint {
	display: flex;
	align-items: center;
	gap: 6px;
	margin: 0;
	padding: 7px 12px;
	border-top: 1px solid var(--border);
	background: var(--bg);
	font-size: 11.5px;
	color: var(--text-3);
}

.compose-tokens {
	margin-top: 10px;
	font-size: 12.5px;
}

.compose-tokens summary {
	cursor: pointer;
	color: var(--text-3);
	display: flex;
	align-items: center;
	gap: 6px;
}

.compose-token-groups {
	display: flex;
	flex-wrap: wrap;
	gap: 14px;
	margin-top: 10px;
}

.compose-token-label {
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: .04em;
	color: var(--text-3);
	margin-bottom: 4px;
}

.compose-token {
	font-family: var(--mono);
	font-size: 11.5px;
	background: var(--surface-2);
	border: 1px solid var(--border);
	border-radius: 6px;
	padding: 2px 6px;
	margin: 0 4px 4px 0;
	cursor: pointer;
	color: var(--text-2);
}

.compose-token:hover {
	border-color: var(--primary);
	color: var(--primary);
}

.compose-attach {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px;
	margin-top: 12px;
}

.compose-chip {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 4px 6px 4px 8px;
	border: 1px solid var(--border);
	border-radius: 999px;
	font-size: 12px;
	background: var(--surface-2);
}

.compose-upload {
	cursor: pointer;
}

.compose-existing {
	max-width: 220px;
}

.compose-foot {
	display: flex;
	align-items: center;
	gap: 10px;
	flex-wrap: wrap;
	padding: 12px 18px;
	border-top: 1px solid var(--border);
	background: var(--surface-2);
}

.compose-foot .hint {
	font-size: 11.5px;
	color: var(--text-3);
	flex: 1;
	min-width: 140px;
}

/* The pending sends in the record's mail card. */
.mail-mini-item.is-pending {
	opacity: .85;
	cursor: default;
}

.mail-mini-row {
	display: flex;
	align-items: center;
	gap: 4px;
}

.mail-mini-row .mail-mini-item {
	flex: 1;
	min-width: 0;
}

/* The generated merge-field reference on the templates screens. */
.token-help {
	display: flex;
	flex-wrap: wrap;
	gap: 22px;
}

.token-help-label {
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: .04em;
	color: var(--text-3);
	margin-bottom: 6px;
}

.token-help-table td {
	padding: 2px 12px 2px 0;
	font-size: 12.5px;
	vertical-align: top;
}

@media (max-width: 820px) {
	.compose-body {
		padding: 12px 14px;
	}

	.compose-head,
	.compose-foot {
		padding: 12px 14px;
	}

	.compose-existing {
		max-width: 100%;
		width: 100%;
	}
}

/* ============================================================ the softphone --

   Phase 55. Three surfaces, one feature:

     .softphone-btn      the idle control in the topbar, beside the bell
     .callbar-phone      the in-call controls, painted into the call bar's slot
     .dialpad            the off-canvas keypad

   Everything here is only ever rendered for somebody whose extension is a
   WebRTC extension — the server decides that, not this stylesheet — so there is
   no "disabled phone" appearance to design. Spacing uses the --space scale
   where it is layout between things and literals where it is a control's own
   internal metrics, which is the rule on /styleguide. */

.softphone-btn {
	width: 32px;
	height: 32px;
	border-radius: 8px;
	display: grid;
	place-items: center;
	color: var(--text-2);
	background: none;
	border: 0;
	cursor: pointer;
	position: relative;
	transition: background .12s, color .12s;
}

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

.softphone-btn .icon {
	width: 17px;
	height: 17px;
	stroke-width: 1.8;
}

/* The registration light. Same geometry as the bell's unread dot, so the two
   controls sit as a pair rather than as two ideas. The colour is never the only
   signal — [data-softphone-status] carries the same state as words in a live
   region, because a colour is not a status to a screen reader and this green
   and this red are the same colour to a good number of people. */
.softphone-dot {
	position: absolute;
	top: 6px;
	right: 6px;
	width: 7px;
	height: 7px;
	border-radius: 50%;
	background: var(--text-3);
	border: 2px solid var(--surface);
	transition: background .15s;
}

.softphone-btn[data-state="registered"] .softphone-dot { background: var(--green); }
.softphone-btn[data-state="connecting"] .softphone-dot,
.softphone-btn[data-state="loading"] .softphone-dot { background: var(--amber); }
.softphone-btn[data-state="failed"] .softphone-dot { background: var(--red); }
.softphone-btn[data-state="elsewhere"] .softphone-dot { background: var(--text-3); }

/* A registered phone is the normal state and should not shout; a phone that is
   trying to come back deserves a glance. Reduced-motion turns the pulse off —
   an animation in the corner of the eye is exactly the kind a vestibular
   disorder reacts to, and the colour still says the same thing. */
@media (prefers-reduced-motion: no-preference) {
	.softphone-btn[data-state="connecting"] .softphone-dot {
		animation: softphone-pulse 1.4s ease-in-out infinite;
	}
}

@keyframes softphone-pulse {
	0%, 100% { opacity: 1; }
	50% { opacity: .35; }
}

/* ---------------------------------------------- the in-call controls -------

   They live inside the call bar, after everything the server rendered, and
   they are the only part of that strip this browser owns. */
/* The keypad and the transfer form hang off the bar, so the bar is the thing
   they hang off. Adding it here rather than to the rule above keeps the whole
   of phase 55's CSS in one block. */
.callbar {
	position: relative;
}

.callbar-phone {
	display: flex;
	align-items: center;
	gap: 6px;
	flex-shrink: 0;
	margin-left: 8px;
}

/* When the softphone has a call the server does not know about — a transfer
   leg, or a call the daemon never saw — the bar's own idle text would sit
   beside live controls saying "No active call". */
.callbar.has-softphone-call .callbar-text {
	display: none;
}

.softphone-ctl {
	white-space: nowrap;
}

.softphone-ctl.is-on {
	background: var(--amber-soft);
	border-color: var(--amber);
	color: var(--text);
}

/* The DTMF pad and the transfer form drop below the strip rather than stretch
   it: the bar has a fixed height that the page layout depends on, and a strip
   that grows by 90px pushes the whole document down mid-call. */
.softphone-keypad,
.softphone-transfer {
	position: absolute;
	right: 14px;
	margin-top: 4px;
	transform: translateY(50%);
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 10px;
	box-shadow: var(--shadow-lg);
	padding: 8px;
	z-index: 40;
}

.softphone-keypad {
	display: grid;
	grid-template-columns: repeat(3, 40px);
	gap: 4px;
}

.softphone-tone {
	height: 34px;
	border-radius: 8px;
	border: 1px solid var(--border);
	background: var(--surface);
	color: var(--text);
	font-size: 14px;
	font-weight: 600;
	cursor: pointer;
}

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

.softphone-transfer {
	display: flex;
	align-items: center;
	gap: 6px;
	flex-wrap: wrap;
	max-width: min(360px, calc(100vw - 28px));
}

.softphone-transfer input {
	width: 150px;
	height: 30px;
	font-size: 12.5px;
}

/* ------------------------------------------------------- the dialpad -------

   In the shell's off-canvas drawer, which is a column at every width. At 390px
   it is the whole screen, which is why the keys are sized in fr rather than in
   pixels: they grow to fill a phone and stay thumb-sized on a laptop. */
.dialpad {
	display: flex;
	flex-direction: column;
	height: 100%;
	padding: 16px 18px 20px;
	overflow-y: auto;
}

.dialpad-head {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 16px;
}

.dialpad-title {
	font-size: 15px;
	font-weight: 650;
	color: var(--text);
}

.dialpad-sub {
	font-size: 12px;
	color: var(--text-2);
	margin-top: 2px;
}

.dialpad-number {
	font-size: 20px;
	letter-spacing: .04em;
	text-align: center;
	height: 46px;
}

.dialpad-keys {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 8px;
	margin: 12px 0 8px;
}

/* 52px is above the 44px touch-target floor with room for the letters. The
   metrics are literals rather than scale steps because this is a control's own
   size, not spacing between things the page composes. */
.dialpad-key {
	height: 52px;
	border-radius: 10px;
	border: 1px solid var(--border);
	background: var(--surface);
	color: var(--text);
	cursor: pointer;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	line-height: 1.05;
	transition: background .1s;
}

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

.dialpad-key:active {
	background: var(--border);
}

.dialpad-key b {
	font-size: 19px;
	font-weight: 600;
}

.dialpad-key span {
	font-size: 9px;
	letter-spacing: .09em;
	color: var(--text-3);
	margin-top: 2px;
}

.dialpad-erase {
	display: flex;
	justify-content: flex-end;
	margin-bottom: 12px;
}

.dialpad-go {
	width: 100%;
	justify-content: center;
	height: 44px;
}

/* Under 820px the call-bar controls keep their icons and lose their words —
   the strip is already full at that width and five labelled buttons would wrap
   it into two rows over the content. Every control keeps its title and its
   aria-label, which is why hiding the text costs nothing but space. */
@media (max-width: 820px) {
	.softphone-ctl span {
		display: none;
	}

	.softphone-ctl {
		padding-left: 8px;
		padding-right: 8px;
	}

	.callbar-phone {
		gap: 4px;
		margin-left: 4px;
	}

	.softphone-transfer input {
		width: 100%;
	}
}

/* ===================================== templates: chrome, slot and preview --

   Phase 56. Three new surfaces:

     .compose-design    the badge in the composer naming the selected design
     .preview           the assembled message in a sandboxed iframe
     .template-*        the admin form where a design is written as HTML

   The editor itself is unchanged, deliberately: the whole point of the phase is
   that the thing in it is now just a message. */

.compose-design {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 8px 10px;
	margin-bottom: 10px;
	border-radius: 8px;
	background: var(--primary-soft);
	border: 1px solid var(--primary-soft-border);
	color: var(--text-2);
	font-size: 12.5px;
}

.compose-design > span {
	flex: 1;
	min-width: 0;
}

/* The source toggle sits in the editor's top-right, over Quill's toolbar. It is
   hidden until email.js has a rich editor to toggle away from — in the plain
   fallback the textarea IS the source and the button would do nothing. */
.compose-editor {
	position: relative;
}

.compose-source-toggle {
	display: none;
	position: absolute;
	top: 6px;
	right: 8px;
	z-index: 2;
	align-items: center;
	gap: 4px;
	padding: 3px 7px;
	border: 1px solid var(--border);
	border-radius: 6px;
	background: var(--surface);
	color: var(--text-3);
	font-size: 11px;
	cursor: pointer;
}

.compose-editor.is-rich .compose-source-toggle,
.compose-editor.is-source .compose-source-toggle {
	display: inline-flex;
}

.compose-source-toggle:hover {
	color: var(--text);
	background: var(--bg);
}

/* In source mode the button is the active control, and the textarea below it
   is no longer a fallback but the thing being used. */
.compose-editor.is-source .compose-source-toggle {
	background: var(--primary-soft);
	border-color: var(--primary);
	color: var(--primary);
}

.compose-editor.is-source .compose-fallback {
	display: block;
	font-family: Consolas, Menlo, monospace;
	font-size: 12px;
}

/* --------------------------------------------------------- the preview -----

   The frame renders a document with its own background, so it gets a neutral
   surround rather than the app's — the same visual grammar as looking at a
   sheet of paper on a desk. */
.compose-preview:empty,
.template-preview:empty {
	display: none;
}

.preview {
	border: 1px solid var(--border);
	border-radius: 10px;
	overflow: hidden;
	margin-top: 12px;
	background: var(--surface);
}

.preview-head {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 10px;
	padding: 10px 12px;
	border-bottom: 1px solid var(--border);
	background: var(--bg);
}

.preview-title {
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: 12.5px;
	font-weight: 600;
	color: var(--text);
}

.preview-meta {
	margin-top: 3px;
	font-size: 11.5px;
	color: var(--text-2);
	overflow-wrap: anywhere;
}

/* A fixed height with its own scrollbar. The drawer scrolls too, and a frame
   sized to its content turns one scroll region into two that fight each other
   under the same finger. */
/*
 * THE PREVIEW FRAME, AND THE REASON IT HAS A HEIGHT AT ALL.
 *
 * The frame is sandboxed with nothing granted, so the document inside cannot
 * measure itself and tell us how tall to be — an iframe therefore has whatever
 * height CSS gives it, and its content scrolls INSIDE that.
 *
 * 420px was too short to read a real message in and gave no sign that there was
 * more below it, which is what "the preview does not scroll" was: the scrolling
 * worked and nothing said it was there. So the frame is now most of the screen,
 * `scrolling="yes"` says so out loud for the few engines that still read it, and
 * the panel around it carries a line telling the reader to scroll. The Taller
 * toggle (the reading pane's, reused) doubles it for a long design.
 */
.preview-frame {
	display: block;
	width: 100%;
	height: min(62vh, 620px);
	border: 0;
	background: #ffffff;
}

.preview.is-tall .preview-frame {
	height: min(88vh, 1100px);
}

.preview-scroll-note {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 6px 12px;
	border-top: 1px solid var(--border);
	background: var(--bg);
	font-size: 11.5px;
	color: var(--text-3);
}

.preview-text {
	border-top: 1px solid var(--border);
	padding: 8px 12px;
	font-size: 12px;
	color: var(--text-2);
}

.preview-text > summary {
	cursor: pointer;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	list-style: none;
}

.preview-text pre {
	margin: 8px 0 0;
	padding: 8px;
	background: var(--bg);
	border-radius: 6px;
	max-height: 180px;
	overflow: auto;
	white-space: pre-wrap;
	font-size: 11.5px;
}

/* ------------------------------------------------- the design editor -------

   Monospace and tall, because it is source. The `[hidden]` rule is what makes
   the two halves exclusive; without `display:none !important` the flex and
   grid children in there would out-specify the attribute. */
.template-kind {
	border: 0;
	padding: 0;
	margin: 0 0 14px;
}

.template-kind > legend {
	font-size: 12.5px;
	font-weight: 600;
	color: var(--text-2);
	margin-bottom: 6px;
	padding: 0;
}

.template-kind .flag {
	align-items: flex-start;
	margin-bottom: 6px;
}

.template-half[hidden] {
	display: none;
}

.template-chrome {
	font-family: Consolas, Menlo, monospace;
	font-size: 12px;
	line-height: 1.5;
	min-height: 320px;
	white-space: pre;
	overflow-wrap: normal;
	overflow-x: auto;
}

.template-preview-bar {
	display: flex;
	align-items: center;
	gap: 10px;
	flex-wrap: wrap;
	margin-top: 10px;
}

/* At phone width the preview frame is the whole point of the screen, so it
   keeps its height while everything around it compresses. The chrome textarea
   shrinks, because nobody writes a table design on a phone — but it must still
   be readable if somebody opens the page there. */
@media (max-width: 820px) {
	.preview-frame {
		height: min(56vh, 460px);
	}

	.template-chrome {
		min-height: 200px;
	}

	.preview-head {
		flex-wrap: wrap;
	}
}

/* ================================================== the softphone's audio --

   Phase 57. The panel behind the settings cog in the dialpad: permission,
   levels, devices. It exists because a phone that connects perfectly and picks
   up no sound fails in front of a customer, and the agent finds out last.

   Collapsed by default (`hidden` on the element, toggled by softphone.js), so
   somebody who opened the drawer to make a call sees a keypad. */

.dialpad-head-actions {
	display: flex;
	align-items: center;
	gap: var(--space-1);
}

/* The registration word beside the extension. It carries the same state the
   dot does, in text, for the same reason the dot is never the only signal. */
.softphone-state[data-state="registered"] { color: var(--green); }
.softphone-state[data-state="connecting"],
.softphone-state[data-state="loading"] { color: var(--amber); }
.softphone-state[data-state="failed"] { color: var(--red); }

.softphone-audio {
	border: 1px solid var(--border);
	border-radius: 10px;
	padding: 14px;
	margin-bottom: var(--space-4);
	background: var(--surface-2);
}

.softphone-audio[hidden] { display: none; }

.softphone-audio .field { margin-bottom: var(--space-3); }

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

/* One permission sentence at a time — softphone.js unhides the right one. */
.softphone-perm {
	display: flex;
	flex-direction: column;
	gap: var(--space-2);
	padding: 10px 12px;
	border-radius: 8px;
	margin-bottom: var(--space-3);
	background: var(--primary-soft);
	border: 1px solid var(--primary-soft-border);
	font-size: 13px;
	line-height: 1.5;
}

.softphone-perm[hidden] { display: none; }

.softphone-perm.is-denied,
.softphone-perm.is-error {
	background: var(--red-soft);
	border-color: var(--red);
}

.softphone-perm-text {
	display: flex;
	gap: var(--space-2);
	align-items: flex-start;
	color: var(--text-2);
}

.softphone-perm-text .icon {
	flex: none;
	margin-top: 2px;
}

.softphone-perm .btn { align-self: flex-start; }

/* THE METER. A track and a fill whose width is the level — no transition on
   the width, because a 60fps value with a 120ms ease is a bar that lags behind
   the voice and reads as a broken microphone rather than a smoothed one. */
.softphone-meter {
	height: 8px;
	border-radius: 4px;
	background: var(--border);
	overflow: hidden;
	margin: 8px 0 6px;
}

.softphone-meter-fill {
	display: block;
	height: 100%;
	width: 0;
	border-radius: 4px;
	/* Green through the useful range; the gradient's top end is only reached
	   by a level that is genuinely too hot to be flattering. */
	background: linear-gradient(90deg, var(--green) 0%, var(--green) 70%, var(--amber) 88%, var(--red) 100%);
	background-size: 220px 100%;
	background-repeat: no-repeat;
}

.softphone-audio-actions {
	display: flex;
	align-items: center;
	gap: var(--space-3);
	flex-wrap: wrap;
	margin-top: 8px;
}

.softphone-audio-actions .hint { margin: 0; }

/* At 390px the drawer is the whole screen and the two pickers are the widest
   things in it — a device label can be sixty characters of USB descriptor. */
@media (max-width: 820px) {
	.softphone-audio { padding: 12px; }

	.softphone-audio .select {
		width: 100%;
		max-width: 100%;
	}

	.softphone-audio-actions { align-items: flex-start; }
}

/* ============================================================================
   TASKS — the checklist card, the worklist, and the checklist admin
   ============================================================================

   ONE SET OF CLASSES FOR THREE PLACES. `.task-list` / `.task-item` render the
   sidebar card, the my-tasks worklist and the template editor, so a checklist
   line looks the same wherever somebody meets it. The differences are
   modifiers on the item (`.is-done`, `.is-template`) rather than parallel
   trees, because two nearly-identical lists is two places to fix a bug in.

   Layout spacing uses the --space-* scale (CLAUDE.md); the metrics INSIDE a
   control — the box's 17px, the check icon's inset — are literals, because
   they are tuning a control against a line box rather than composing a page. */

.tasks-card {
	display: flex;
	flex-direction: column;
	gap: var(--space-3);
}

.tasks-count {
	font-size: 12.5px;
	color: var(--text-2);
}

.tasks-count b {
	color: var(--text);
	font-size: 14px;
}

.task-list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: var(--space-1);
}

.task-item {
	display: flex;
	align-items: flex-start;
	gap: var(--space-2);
	padding: 6px 0;
	border-bottom: 1px solid var(--border);
}

.task-item:last-child { border-bottom: none; }

/* THE BOX. A real button, sized to sit on the first line of the title beside
   it rather than centred against the whole item — a two-line task with a
   vertically-centred box reads as a box belonging to neither line. */
.task-check {
	flex: 0 0 auto;
	width: 17px;
	height: 17px;
	margin-top: 2px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border: 1.5px solid var(--border-strong);
	border-radius: 5px;
	background: var(--surface);
	color: var(--on-primary);
	cursor: pointer;
	transition: background .12s, border-color .12s;
}

.task-check:hover { border-color: var(--primary); }

.task-check.is-static { cursor: default; }

.task-item.is-done .task-check {
	background: var(--primary);
	border-color: var(--primary);
}

.task-check .icon {
	width: 12px;
	height: 12px;
}

.task-check-form { display: contents; }

.task-body {
	flex: 1 1 auto;
	min-width: 0;
}

.task-title {
	font-size: 13px;
	color: var(--text);
	overflow-wrap: anywhere;
}

.task-item.is-done .task-title {
	color: var(--text-3);
	text-decoration: line-through;
}

.task-note,
.task-meta {
	font-size: 12px;
	color: var(--text-3);
	margin-top: 2px;
}

.task-kill {
	flex: 0 0 auto;
	opacity: 0;
	transition: opacity .12s;
}

/* Revealed on hover on a pointer, and ALWAYS present on touch — where there is
   no hover and a permanently invisible delete is a missing feature. */
.task-item:hover .task-kill,
.task-item:focus-within .task-kill { opacity: 1; }

@media (hover: none) {
	.task-kill { opacity: 1; }
}

/* The add form stacks (title on its own line, then owner + button); the apply
   form is one row, because a select and a button is all it is. They are
   deliberately NOT in one rule with a shared `flex-direction`: written that
   way the apply row inherited `column` and put its button under its select,
   centred, looking like a mistake. */
.task-add {
	display: flex;
	flex-direction: column;
	gap: var(--space-1);
}

.task-add-row,
.task-apply {
	display: flex;
	align-items: center;
	gap: var(--space-1);
}

.task-add-row .select,
.task-apply .select {
	flex: 1 1 auto;
	min-width: 0;
}

/* ---------------------------------------------------------- the worklist -- */

.task-groups {
	display: flex;
	flex-direction: column;
	gap: var(--space-4);
}

.task-group {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: var(--space-3);
}

.task-group-head {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	margin-bottom: var(--space-2);
}

.task-group-title {
	font-weight: 600;
	font-size: 13.5px;
	color: var(--text);
}

/* The module that owns this record is switched off: the cached label is all
   that is left, so it is shown and marked rather than linked into a 404. */
.task-group-title.is-gone {
	color: var(--text-3);
	font-style: italic;
}

/* ------------------------------------------------- the template editor -- */

.task-item.is-template { align-items: center; }

.task-item-form {
	display: flex;
	align-items: center;
	gap: var(--space-1);
	flex-wrap: wrap;
}

.task-item-form input[type=text] { flex: 1 1 180px; min-width: 0; }

.task-item-tools {
	flex: 0 0 auto;
	display: flex;
	align-items: center;
	gap: 2px;
}

/* At 390px the sidebar card is the full width and the add-row's select plus
   button no longer fit on one line beside each other. */
@media (max-width: 820px) {
	.task-add-row,
	.task-apply {
		flex-wrap: wrap;
	}

	.task-item-form { width: 100%; }

	.task-item-tools { align-self: flex-start; }
}

/* ============================================================================
   REPORTS — the catalogue, the filter bar, the chart and the table
   ============================================================================

   Everything here composes with the --space-* scale (CLAUDE.md); the only
   literal metrics are inside a control or a chart box, where the number is
   tuning a shape rather than laying out a page. */

/* Right-aligned numerics, used by every report table and by the checklist
   admin. General on purpose: a column of numbers reads down its last digit, and
   the alternative is each screen inventing its own alignment. */
th.num,
td.num {
	text-align: right;
	white-space: nowrap;
}

.report-group {
	margin-bottom: var(--space-4);
}

.report-group-head {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	margin-bottom: var(--space-2);
}

.report-group-title {
	font-weight: 600;
	font-size: 13.5px;
	color: var(--text);
}

.report-cards {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
	gap: var(--space-2);
}

.report-card {
	display: flex;
	align-items: flex-start;
	gap: var(--space-2);
	padding: var(--space-3);
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	color: inherit;
	transition: border-color .12s, box-shadow .12s;
}

a.report-card:hover,
.report-card:focus-within {
	border-color: var(--primary);
	box-shadow: var(--shadow-sm);
}

.report-card-kind {
	flex: 0 0 auto;
	width: 28px;
	height: 28px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border-radius: 7px;
	background: var(--primary-soft);
	color: var(--primary);
}

.report-card-body {
	flex: 1 1 auto;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 2px;
	color: inherit;
}

.report-card-title {
	font-size: 13px;
	font-weight: 600;
	color: var(--text);
}

.report-card-title.is-gone {
	color: var(--text-3);
	font-style: italic;
}

.report-card-sub {
	font-size: 12px;
	color: var(--text-3);
	overflow-wrap: anywhere;
}

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

.report-controls {
	display: flex;
	align-items: flex-end;
	gap: var(--space-2);
	flex-wrap: wrap;
	padding: var(--space-3);
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	margin-bottom: var(--space-3);
}

.report-controls .field {
	margin: 0;
	min-width: 130px;
}

.report-controls .field label {
	font-size: 11.5px;
}

/* --------------------------------------------------------------- answer -- */

.report-summary {
	display: flex;
	align-items: baseline;
	gap: var(--space-3);
	flex-wrap: wrap;
	margin-bottom: var(--space-3);
}

.report-total-value {
	font-size: 26px;
	font-weight: 600;
	color: var(--text);
	margin-right: var(--space-1);
}

.report-total-label {
	font-size: 13px;
	color: var(--text-2);
}

/* Whose numbers these are. Quiet, but never absent — see body.latte. */
.report-scope {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	font-size: 12px;
	color: var(--text-3);
}

.report-note {
	flex: 1 1 100%;
	font-size: 12px;
	color: var(--text-3);
}

/* The chart gets the room; the table sits beside it and scrolls on its own. */
.report-panels {
	display: grid;
	grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
	gap: var(--space-3);
	align-items: start;
}

/* A canvas has no intrinsic height, and Chart.js with
   `maintainAspectRatio: false` fills its parent — so the parent must have one
   or the chart collapses to nothing. */
.report-chart {
	position: relative;
	height: 320px;
}

.report-chart canvas {
	max-width: 100%;
}

.report-table-card .card-body {
	padding-top: 0;
}

/* ------------------------------------------------ the dashboard card (P61) --

   A curated report shrunk to a widget: a total, a short chart, a scope line and
   a way through to the full thing. No table — a dashboard card is a shape you
   read at a glance, and the figures are one click away on the report itself.

   The chart is the SAME markup the report page renders; only this height
   differs, which is why it is a modifier rather than a second component. */

.report-widget {
	display: flex;
	flex-direction: column;
	gap: var(--space-2);
}

.report-widget-head {
	display: flex;
	align-items: baseline;
	gap: var(--space-2);
	flex-wrap: wrap;
	min-width: 0;
}

.report-widget-total {
	font-size: 22px;
	font-weight: 650;
	line-height: 1.1;
	color: var(--text-1);
}

.report-widget-metric {
	font-size: 12px;
	color: var(--text-3);
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.report-chart.is-compact {
	height: 180px;
}

.report-widget-foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-2);
	flex-wrap: wrap;
	font-size: 12px;
}

.report-widget-scope {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	min-width: 0;
	color: var(--text-3);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.report-widget-link {
	display: inline-flex;
	align-items: center;
	gap: 3px;
	color: var(--primary);
	font-weight: 550;
	white-space: nowrap;
}

/* A widget that has nothing to draw — no data yet, or the module that publishes
   the source is switched off. Same shape either way, because "empty" and
   "absent" are both states to explain rather than blanks to leave. */
.widget-note {
	display: flex;
	align-items: flex-start;
	gap: var(--space-2);
	font-size: 13px;
	color: var(--text-2);
}

.widget-note svg {
	flex: 0 0 auto;
	margin-top: 2px;
	color: var(--text-3);
}

.widget-note .muted {
	font-size: 12px;
	margin-top: 2px;
}

/* ----------------------------------------------------------- saved view -- */

.report-save {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	flex-wrap: wrap;
	margin-top: var(--space-3);
	padding-top: var(--space-3);
	border-top: 1px solid var(--border);
}

.report-save input[type=text] {
	min-width: 220px;
}

/* At 390 the two panels stack, the controls become one per line, and the chart
   loses a little height so the table is not below the fold on every report. */
@media (max-width: 820px) {
	.report-panels {
		grid-template-columns: 1fr;
	}

	.report-chart {
		height: 260px;
	}

	.report-controls {
		flex-direction: column;
		align-items: stretch;
	}

	.report-controls .field,
	.report-controls .btn {
		width: 100%;
	}

	.report-cards {
		grid-template-columns: 1fr;
	}
}

/* ---------------------------------------------------------------------------
   AI ASSIST — the composer control and the suggestion it returns
   ---------------------------------------------------------------------------

   Here rather than in the ai module because no module ships CSS: the tokens and
   components sheets are the whole vocabulary, and a module that grew its own
   stylesheet would be a module whose spacing drifts from everything around it.

   The suggestion is deliberately quiet. It is a PROPOSAL sitting next to work
   somebody already did, so it gets a dashed border and the page background —
   not a card, not an accent, nothing that reads as "this is the real one now". */

.ai-assist-row {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	margin-top: var(--space-2);
	flex-wrap: wrap;
}

/* `.ai-assist` also carries `.dropdown`, which owns position and display. Only
   the indicator behaviour is added here. */
.ai-assist.htmx-request {
	opacity: .55;
}

/* THE MENU OPENS UPWARD, and that is not a preference.

   The control sits under the editor, a few pixels above the composer drawer's
   sticky footer. `.dropdown-menu`'s default (below, right-aligned) put a
   ten-item menu behind the Send bar at every width — visible for three rows and
   clipped for the rest, which reads as a broken menu rather than a scrolled
   one. Above the button is the editor, which is always the taller side.

   Left-aligned for the same reason: right-aligned, a 240px menu hangs off the
   left edge of a 390px panel. */
.ai-menu {
	top: auto;
	bottom: calc(100% + 6px);
	left: 0;
	right: auto;
	min-width: 240px;
	max-height: 60vh;
	overflow-y: auto;
}

.ai-prompt .ai-instruction {
	min-height: 72px;
}

.ai-prompt .field {
	margin-top: var(--space-2);
}

.ai-prompt-note {
	margin: var(--space-2) 0 0;
	font-size: 11.5px;
}

.ai-assist-note {
	font-size: 12px;
	line-height: 1.4;
}

.ai-suggestion {
	margin-top: var(--space-2);
	padding: var(--space-2);
	border: 1px dashed var(--border);
	border-radius: var(--radius);
	background: var(--bg);
}

.ai-suggestion-head {
	display: flex;
	align-items: center;
	gap: 6px;
	flex-wrap: wrap;
	margin-bottom: var(--space-1);
	font-size: 12.5px;
}

.ai-suggestion-text {
	min-height: 140px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--surface);
}

.ai-suggestion-actions {
	display: flex;
	gap: var(--space-1);
	margin-top: var(--space-2);
}

/* Under 820 the note stops sharing a line with the button — beside a 170px
   button in a 390px drawer it wraps into two ragged lines that read as broken
   rather than as a caption. */
@media (max-width: 820px) {
	.ai-assist-row {
		align-items: flex-start;
		flex-direction: column;
		gap: var(--space-1);
	}

	.ai-menu {
		min-width: 0;
		width: calc(100vw - 64px);
	}

	.ai-suggestion-actions {
		flex-direction: column;
	}

	.ai-suggestion-actions .btn {
		width: 100%;
	}
}

/* ---------------------------------------------------------------------------
   AI INSIGHTS — the analyse control, the reading, and the proposal
   ---------------------------------------------------------------------------

   Same reasoning as the AI Assist block above: no module ships CSS, so the
   vocabulary is here where the spacing scale and the tokens are. */

.ai-analyze {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	margin-bottom: var(--space-3);
	flex-wrap: wrap;
}

/* `display: block` is what .htmx-indicator turns on; inline-flex would fight
   it, so the alignment is done with the vertical-align a flex parent gives. */
.ai-analyzing {
	font-size: 12px;
	color: var(--text-3);
}

.ai-insights,
.ai-proposal,
.ai-proposal-done {
	margin-bottom: var(--space-3);
	padding: var(--space-3);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface);
}

.ai-insights.htmx-request,
.ai-proposal.htmx-request {
	opacity: .55;
}

.ai-insights-again {
	margin-left: auto;
}

.ai-insight-summary {
	margin: var(--space-2) 0 0;
	font-size: 13.5px;
	line-height: 1.6;
}

.ai-insight-chips {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-1);
	margin-top: var(--space-3);
}

/* A chip carries its meaning in the WORD; the colour is a second channel for
   people who read at a glance, never the only one. */
.ai-chip {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	padding: 3px 9px;
	border: 1px solid var(--border);
	border-radius: 999px;
	background: var(--bg);
	font-size: 11.5px;
	color: var(--text-2);
}

.ai-chip-positive { border-color: var(--green); color: var(--green); }
.ai-chip-negative { border-color: var(--red); color: var(--red); }
.ai-chip-urgency-high { border-color: var(--red); color: var(--red); }
.ai-chip-urgency-low { color: var(--text-3); }

.ai-insight-entities {
	margin-top: var(--space-3);
	padding-top: var(--space-3);
	border-top: 1px solid var(--border);
}

.ai-insight-entities-head {
	font-size: 11.5px;
	margin-bottom: var(--space-2);
}

/* Two columns on a comfortable screen, one below — a definition list is the
   right element for label/value and needs the grid to stop being a stack. */
.ai-insight-entities dl {
	display: grid;
	grid-template-columns: minmax(90px, 160px) 1fr;
	gap: 4px var(--space-3);
	margin: 0;
	font-size: 12.5px;
}

.ai-insight-entities dt {
	color: var(--text-3);
}

.ai-insight-entities dd {
	margin: 0;
	color: var(--text);
}

.ai-insight-actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2);
	margin-top: var(--space-3);
}

.ai-insight-foot {
	margin: var(--space-2) 0 0;
	font-size: 11.5px;
}

.ai-proposal-fields {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--space-2) var(--space-3);
	margin-top: var(--space-3);
}

.ai-proposal-was {
	margin-top: 3px;
}

@media (max-width: 820px) {
	.ai-insight-entities dl,
	.ai-proposal-fields {
		grid-template-columns: 1fr;
	}

	.ai-insight-actions .btn,
	.ai-analyze .btn {
		width: 100%;
	}

	.ai-insights-again {
		margin-left: 0;
	}
}

/* ---------- Phase 74: the global assistant drawer ---------- *
 *
 * A column: fixed header, scrolling transcript, composer pinned to the bottom.
 * The transcript is the only part that grows, which is what lets an answer be
 * APPENDED rather than the panel re-rendered — the caret stays where it was and
 * nothing above flickers.
 */
.ai-assistant {
	display: flex;
	flex-direction: column;
	height: 100%;
	min-height: 0;
}

.ai-assistant-head {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--space-3);
	padding: var(--space-3) var(--space-4);
	border-bottom: 1px solid var(--border);
}

.ai-assistant-head-actions {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	flex-shrink: 0;
}

.ai-assistant-head-main {
	min-width: 0;
}

.ai-assistant-title {
	display: flex;
	align-items: center;
	gap: 6px;
	font-weight: 600;
}

.ai-assistant-sub {
	margin-top: 2px;
	font-size: 12px;
	color: var(--text-2);
}

/* WHAT IT CAN READ AND CHANGE, collapsed into the header. It has to be
 * available three turns in — which is when somebody wonders — without being
 * the first thing on the screen every time the drawer opens. */
.ai-capabilities {
	margin-top: var(--space-2);
	font-size: 12px;
}

.ai-capabilities > summary {
	cursor: pointer;
	display: flex;
	align-items: center;
	gap: 5px;
	color: var(--text-3);
	user-select: none;
}

.ai-capabilities > summary:hover {
	color: var(--text-2);
}

.ai-capabilities p:last-child {
	margin-bottom: 0;
}

/* The scroller. `min-height: 0` because a flex child will not shrink below its
 * content otherwise, and the composer would be pushed off the bottom. */
.ai-assistant-log {
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
	padding: var(--space-4);
	display: flex;
	flex-direction: column;
	gap: var(--space-4);
}

/* THE CONVERSATION ITSELF, which is a separate box from the scroller so that
 * the typing bubble — a sibling BELOW it — is the last thing in the drawer
 * without any script having to keep it there.
 *
 * `margin-top: auto` bottom-aligns a short conversation, which is what a chat
 * does: two messages sit above the box you are typing in, not stranded at the
 * top of a tall empty column. An auto margin only consumes FREE space, so once
 * the transcript is taller than the scroller it collapses to nothing and the
 * whole thing scrolls normally — unlike `justify-content: flex-end`, which
 * makes the overflowing top unreachable. */
.ai-assistant-stream {
	display: flex;
	flex-direction: column;
	gap: var(--space-4);
	min-width: 0;
	margin-top: auto;
}

/* THE OPENING SCREEN, out of the way the moment there is a conversation. A
 * chat that keeps its welcome above every answer reads as a page.
 *
 * Both selectors, because the question appears BEFORE the answer does: the
 * echoed bubble is what is on screen for the fifteen to forty seconds a local
 * model takes, and leaving the welcome up for that is the state somebody
 * actually sees.
 *
 * `:has()` and not a class toggled by script: the condition IS "there is
 * something in the stream", and where a browser does not support it the empty
 * state simply stays — which is exactly what it did before this rule existed. */
.ai-assistant-stream:has(.ai-exchange) .ai-assistant-empty,
.ai-assistant-stream:has([data-ai-echo]) .ai-assistant-empty {
	display: none;
}

.ai-assistant-empty {
	text-align: center;
	padding: var(--space-4) 0 var(--space-2);
}

.ai-assistant-empty-mark {
	width: 44px;
	height: 44px;
	margin: 0 auto var(--space-3);
	border-radius: 50%;
	display: grid;
	place-items: center;
	background: var(--primary-soft);
	color: var(--primary);
}

.ai-assistant-empty-lead {
	margin: 0 0 var(--space-3);
	font-weight: 600;
}

.ai-assistant-empty p {
	margin: 0 0 var(--space-2);
}

/* The examples. Rendered as list items and upgraded into buttons by
 * assistant.js — so `role="button"` is what the pointer and the hover state
 * hang off, and without the script they are three sentences to read. */
.ai-assistant-examples {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: var(--space-2);
	font-size: 13px;
}

.ai-assistant-examples li {
	padding: 8px 12px;
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface);
	color: var(--text-2);
}

.ai-assistant-examples li[role="button"] {
	cursor: pointer;
	transition: border-color .12s ease, color .12s ease, background .12s ease;
}

.ai-assistant-examples li[role="button"]:hover,
.ai-assistant-examples li[role="button"]:focus-visible {
	border-color: var(--primary-soft-border);
	background: var(--primary-soft);
	color: var(--text);
	outline: none;
}

.ai-assistant-can-see {
	font-size: 12px;
}

.ai-exchange {
	display: flex;
	flex-direction: column;
	gap: var(--space-4);
}

/* =========================================================================
 * A TURN — a mark, and a column
 * =========================================================================
 *
 * The assistant's side is a small sparkles badge and, beside it, a column
 * holding the bubble and everything that belongs to that answer: the proposal
 * card, the trace, the note about a truncated chain. Stacked UNDER the bubble
 * at the same indent, they read as part of the answer without being mistaken
 * for the words the model said.
 *
 * The person's side has no badge — the alignment is the label, and a second
 * avatar in a two-party conversation is decoration. */
.ai-turn {
	display: flex;
	align-items: flex-start;
	gap: var(--space-2);
	max-width: 100%;
	min-width: 0;
}

.ai-turn-user {
	justify-content: flex-end;
}

.ai-turn-avatar {
	flex: 0 0 auto;
	width: 24px;
	height: 24px;
	margin-top: 1px;
	border-radius: 50%;
	display: grid;
	place-items: center;
	background: var(--primary-soft);
	color: var(--primary);
}

.ai-turn-main {
	min-width: 0;
	max-width: 100%;
}

/* The assistant's column takes the width it needs; the person's is capped, so
 * a short question does not stretch into a full-width banner. */
.ai-turn-assistant .ai-turn-main {
	flex: 1 1 auto;
}

.ai-turn-user .ai-turn-main {
	max-width: 85%;
}

.ai-turn-body {
	/* The answer is TEXT from a model, escaped by Latte. `pre-wrap` is what
	 * makes the list it wrote look like a list without any of it being markup. */
	white-space: pre-wrap;
	overflow-wrap: anywhere;
	line-height: 1.55;
	padding: 9px 12px;
	border-radius: var(--radius);
	border: 1px solid var(--border);
	background: var(--surface-2);
}

/* The question: the app's own accent, and the corner nearest the composer
 * squared off. That notch is the whole "which of us said this" cue, and it
 * survives a screenshot in greyscale where the colour does not. */
.ai-turn-user .ai-turn-body {
	background: var(--primary-soft);
	border-color: var(--primary-soft-border);
	border-bottom-right-radius: 3px;
}

.ai-turn-assistant .ai-turn-body {
	border-bottom-left-radius: 3px;
}

/* The error copy is a callout, which brings its own box — it must not end up
 * inside a second one. */
.ai-turn-body.callout {
	border-radius: var(--radius);
	white-space: normal;
}

.ai-turn-note {
	margin: var(--space-2) 0 0;
	display: flex;
	align-items: center;
	gap: 5px;
}

/* =========================================================================
 * THE TYPING BUBBLE
 * =========================================================================
 *
 * An ordinary `htmx-indicator` sitting after the stream, so it is always last
 * and htmx's own request lifecycle turns it on and off. No script is involved
 * in showing it: with assistant.js missing, the wait is still narrated.
 *
 * Both selectors carry `.ai-turn`, because `.ai-turn { display: flex }` is
 * declared later in this file than the generic `.htmx-indicator { display:
 * none }` and would otherwise leave the bubble permanently on screen. */
.ai-turn.htmx-indicator {
	display: none;
}

.ai-turn.htmx-indicator.htmx-request {
	display: flex;
}

.ai-typing {
	display: flex;
	align-items: center;
	gap: 9px;
	color: var(--text-2);
	font-size: 13px;
}

.ai-typing-dots {
	display: inline-flex;
	gap: 4px;
	flex-shrink: 0;
}

.ai-typing-dots i {
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--primary);
	opacity: .35;
	animation: ai-typing-pulse 1.2s ease-in-out infinite;
}

.ai-typing-dots i:nth-child(2) { animation-delay: .18s; }
.ai-typing-dots i:nth-child(3) { animation-delay: .36s; }

@keyframes ai-typing-pulse {
	0%, 60%, 100% { opacity: .25; transform: translateY(0); }
	30%           { opacity: 1;   transform: translateY(-2px); }
}

/* The dots are the decoration; the SENTENCE is the message. With animation
 * off, "Reading your CRM…" still says the machine is working — which is the
 * one thing this bubble exists to say. */
@media (prefers-reduced-motion: reduce) {
	.ai-typing-dots i {
		animation: none;
		opacity: .6;
	}
}

/* The trace. Closed by default, and deliberately quiet: it is there to be
 * checked, not read. */
.ai-steps {
	margin-top: var(--space-2);
	font-size: 12px;
}

.ai-steps summary {
	cursor: pointer;
	color: var(--text-3);
	display: flex;
	align-items: center;
	gap: 5px;
	user-select: none;
}

.ai-steps-list {
	margin: var(--space-2) 0 0;
	padding-left: var(--space-4);
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.ai-step {
	color: var(--text-3);
}

.ai-step-tool {
	color: var(--text);
	font-weight: 600;
}

.ai-step-args {
	color: var(--text-3);
}

.ai-step-summary {
	display: block;
}

/* A refused step is the interesting one — it is how somebody finds out the
 * answer was narrower than the question. Coloured AND worded, never colour
 * alone. */
.ai-step-refused .ai-step-summary {
	color: var(--red);
}

/* =========================================================================
 * PHASE 75 — a change the assistant PROPOSED, which nobody has agreed to yet
 * =========================================================================
 *
 * It has to read as a QUESTION rather than as a result. So it is a bordered
 * card inside the exchange, with the same accent the primary button has, and
 * the actions are at the bottom where a decision belongs — not a toast that
 * has already happened.
 *
 * Two weights, and the difference is deliberately quiet: a review card gets a
 * slightly stronger left edge, because the real signal is the `was → now` on
 * every line rather than a colour somebody has to have been taught. */
.ai-proposal-card {
	margin-top: var(--space-3);
	padding: var(--space-3);
	border: 1px solid var(--border);
	border-left: 3px solid var(--primary);
	border-radius: var(--radius);
	background: var(--surface-2);
}

/* When the whole turn IS the card — a proposal the model made without a
 * sentence around it — there is nothing above it for the margin to separate it
 * from, and the gap reads as a missing bubble. */
.ai-turn-main > .ai-proposal-card:first-child {
	margin-top: 0;
}

.ai-proposal-review {
	border-left-width: 4px;
}

.ai-proposal-card-head {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	margin-bottom: var(--space-2);
}

/* A definition list rather than a table: one to five rows inside a drawer that
 * is 320px wide on a phone, and a table there is a horizontal scrollbar. */
.ai-proposal-changes {
	display: grid;
	grid-template-columns: minmax(80px, auto) 1fr;
	gap: 4px var(--space-3);
	margin: 0;
}

.ai-proposal-changes dt {
	color: var(--text-3);
}

.ai-proposal-changes dd {
	margin: 0;
	word-break: break-word;
}

/* Struck through AND muted AND followed by an arrow. Three signals for one
 * fact, because "which of these two is the current value" is the question the
 * whole card exists to answer. */
.ai-proposal-was {
	color: var(--text-3);
}

.ai-proposal-arrow {
	color: var(--text-3);
	margin: 0 4px;
}

.ai-proposal-now {
	font-weight: 600;
}

.ai-proposal-target {
	margin: var(--space-2) 0 0;
}

.ai-proposal-card-actions {
	display: flex;
	gap: var(--space-2);
	margin-top: var(--space-3);
	flex-wrap: wrap;
}

/* A BULK card. The criteria block is what makes the count readable, so it gets
 * the visual weight — a bordered box above the number rather than a footnote
 * under it. */
.ai-proposal-bulk {
	border-left-width: 4px;
}

.ai-bulk-filter {
	margin: var(--space-2) 0;
	padding: var(--space-2) var(--space-3);
	border: 1px dashed var(--border);
	border-radius: var(--radius);
}

.ai-bulk-filter-label {
	color: var(--text-3);
	font-size: 0.85em;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	margin-bottom: 4px;
}

.ai-bulk-criteria {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-wrap: wrap;
	gap: 2px var(--space-3);
}

.ai-bulk-count {
	margin: var(--space-2) 0;
	font-size: 1.05em;
}

.ai-bulk-note {
	margin: var(--space-2) 0 0;
}

/* What replaced the card. Never nothing: a card that simply vanishes looks
 * exactly like one that silently went through. */
.ai-proposal-outcome {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	flex-wrap: wrap;
	margin-top: var(--space-3);
	padding: var(--space-2) var(--space-3);
	border-radius: var(--radius);
	background: var(--surface-2);
}

.ai-proposal-done {
	color: var(--green);
}

.ai-proposal-cancelled,
.ai-proposal-refused {
	color: var(--text-3);
}

.ai-proposal-refused {
	color: var(--red);
}

/* =========================================================================
 * THE COMPOSER
 * =========================================================================
 *
 * The border is on the WRAPPER and the textarea is bare inside it, so the send
 * button sits in the field the way a chat composer does rather than under it —
 * and the whole thing shows one focus ring, on the wrapper, via
 * `:focus-within`. A bordered textarea next to a bordered button is two
 * controls; this is one. */
.ai-assistant-composer {
	flex: 0 0 auto;
	border-top: 1px solid var(--border);
	padding: var(--space-3) var(--space-4);
	background: var(--surface);
}

.ai-composer-box {
	display: flex;
	align-items: flex-end;
	gap: 6px;
	padding: 5px 5px 5px 11px;
	border: 1px solid var(--border-strong);
	border-radius: var(--radius);
	background: var(--surface);
	transition: border-color .12s ease, box-shadow .12s ease;
}

.ai-composer-box:focus-within {
	border-color: var(--primary);
	box-shadow: 0 0 0 3px var(--primary-soft);
}

/* A textarea with no chrome of its own. `resize: none` because the box grows
 * itself; a drag handle in the corner of a growing field fights the script and
 * leaves the button hanging off the bottom.
 *
 * `max-height` is what bounds it when assistant.js is missing, and it must
 * agree with MAX_COMPOSER_PX in that file. */
.ai-composer-box textarea {
	flex: 1 1 auto;
	min-width: 0;
	border: 0;
	padding: 6px 0;
	background: transparent;
	resize: none;
	max-height: 168px;
	overflow-y: auto;
	line-height: 1.5;
	font: inherit;
	color: inherit;
}

.ai-composer-box textarea:focus {
	outline: none;
	box-shadow: none;
}

.ai-composer-box textarea:disabled {
	background: transparent;
	color: var(--text-3);
}

/* Round, filled, and it stays put as the box grows: `align-items: flex-end` on
 * the wrapper keeps it on the last line rather than floating beside the
 * first. */
.ai-composer-send {
	flex: 0 0 auto;
	width: 32px;
	height: 32px;
	border-radius: 50%;
	background: var(--primary);
	color: #fff;
	border: 0;
}

.ai-composer-send:hover:not(:disabled) {
	background: var(--primary-hover);
	color: #fff;
}

.ai-composer-send:disabled {
	opacity: .55;
	cursor: not-allowed;
}

.ai-composer-hint {
	margin: 6px 2px 0;
	font-size: 11px;
}

@media (max-width: 820px) {
	.ai-assistant-head,
	.ai-assistant-log,
	.ai-assistant-composer {
		padding-left: var(--space-3);
		padding-right: var(--space-3);
	}

	/* A 390px drawer has no room to give 15% of the width away — but it cannot
	 * go to 100% either: with the gutter gone the question is the same width as
	 * the answer and the alignment stops saying anything. Just enough to see. */
	.ai-turn-user .ai-turn-main {
		max-width: 92%;
	}
}

/* ---------- two-step verification ---------- */

/*
 * The enrolment block: a QR beside the key it encodes.
 *
 * Two columns where there is room and one where there is not, because the
 * fallback for a camera that will not focus is typing 32 characters, and 32
 * characters wrapped to four per line is not typeable. The QR keeps its
 * intrinsic size — a scaled-down one is a QR nothing can read.
 */
.totp-enrol {
	display: flex;
	gap: var(--space-4);
	align-items: flex-start;
	flex-wrap: wrap;
}

.totp-qr {
	flex: 0 0 auto;
	line-height: 0;
	padding: 10px;
	background: #fff;
	border: 1px solid var(--border);
	border-radius: var(--radius);
}

.totp-qr svg {
	display: block;
	width: 180px;
	height: 180px;
}

.totp-enrol > div:last-child {
	flex: 1 1 260px;
	min-width: 240px;
}

/*
 * The password and the button that uses it, on one line where there is room.
 * `align-items: flex-end` so the button sits on the input's baseline rather
 * than beside its label.
 */
.totp-disable {
	display: flex;
	gap: var(--space-2);
	align-items: flex-end;
	flex-wrap: wrap;
}

/*
 * A security setting on a person's own profile: what it is on the left, the
 * one control on the right.
 *
 * It wraps rather than shrinks — a button that has been squeezed to two
 * characters at 390px is a button nobody presses — and the control drops to
 * the full width of the panel when it does, so the tap target stays honest.
 */
.security-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-3);
	flex-wrap: wrap;
}

.security-row-main {
	display: grid;
	gap: 2px;
	min-width: 240px;
	flex: 1 1 320px;
}

.security-row-title {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	flex-wrap: wrap;
}

@media (max-width: 520px) {
	.security-row > .btn {
		width: 100%;
		justify-content: center;
	}
}
