/* Shared Bootstrap bridges and cross-surface components. */

:root {
  --bs-body-bg: var(--bg);
  --bs-body-color: var(--text);
  --bs-border-color: var(--border);
  --bs-link-color: var(--accent-primary);
  --bs-link-hover-color: var(--accent-primary);
  --bs-secondary-color: var(--text-muted);
  --bs-tertiary-bg: var(--bg);
  --bs-font-sans-serif: "PT Sans", system-ui, sans-serif;
}

html,
body {
  min-height: 100%;
  /* A guard: nothing may widen a phone's page sideways. `clip`, not
     `hidden`, so `position: sticky` keeps working. */
  overflow-x: clip;
}

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
}

a {
  color: var(--accent-primary);
  text-decoration: none;
}

a:hover,
a:focus {
  color: var(--accent-primary);
  text-decoration: none;
}

/* One transition for every link, button and disclosure on every surface.
   Nearly all of the project's interactive chrome -- filter chips, breadcrumbs,
   contact and social links, entity nav, card chips, footer links, the image
   upload buttons, the sidebar toggles -- is an `a` or a `button`, so this
   reaches them without twenty separate declarations to keep in step.

   Element selectors on purpose: Bootstrap's `.btn` and `.nav-link` are class
   rules and outrank this, so vendor components keep their own timing and their
   own reduced-motion handling. Only the properties that actually change on
   hover are listed -- `.skip-link:focus` moves with `top`, and a skip link that
   eases into view wastes the time of the one person who needed it. */
a,
button,
summary {
  transition:
    color var(--motion-fast) var(--motion-ease),
    background-color var(--motion-fast) var(--motion-ease),
    border-color var(--motion-fast) var(--motion-ease);
}

/* The focus ring, for every surface. It began scoped to the public marketplace,
   which left the CRM and the dashboard falling back to Bootstrap's blue on the
   same components. Per-component rings in the surface sheets still override it,
   because those load after this one. */
:is(a, button, input, select, textarea, summary):focus-visible {
  outline: var(--focus-outline);
  outline-offset: 3px;
}

h1,
h2,
h3,
h4,
h5,
h6,
.navbar-brand {
  font-family: var(--font-heading);
  /* Raleway defaults to old-style figures, where 0 reads as the letter o:
     "0 ta kurs", "0 dan". Headings carry counts, years and prices. */
  font-variant-numeric: lining-nums;
}

.portal-layout,
.dashboard-shell {
  background: var(--bg);
  color: var(--text);
}

.portal-navbar,
.dashboard-sidebar,
.portal-footer,
.auth-navbar,
.auth-card {
  background: var(--surface);
  border-color: var(--border);
}

.portal-navbar .nav-link,
.auth-navbar .nav-link,
.portal-navbar .navbar-brand,
.auth-navbar .navbar-brand {
  color: var(--text);
}

.portal-navbar .nav-link:hover,
.portal-navbar .nav-link:focus,
.auth-navbar .nav-link:hover,
.auth-navbar .nav-link:focus,
.portal-navbar .navbar-brand:hover,
.portal-navbar .navbar-brand:focus,
.auth-navbar .navbar-brand:hover,
.auth-navbar .navbar-brand:focus {
  color: var(--accent-primary);
}

.btn-primary {
  --bs-btn-bg: var(--accent-primary);
  --bs-btn-border-color: var(--accent-primary);
  --bs-btn-hover-bg: var(--accent-primary);
  --bs-btn-hover-border-color: var(--accent-primary);
  --bs-btn-active-bg: var(--accent-primary);
  --bs-btn-active-border-color: var(--accent-primary);
  --bs-btn-color: var(--surface);
  --bs-btn-hover-color: var(--surface);
  --bs-btn-active-color: var(--surface);
}

.btn-outline-primary {
  --bs-btn-color: var(--accent-primary);
  --bs-btn-border-color: var(--accent-primary);
  --bs-btn-hover-bg: var(--accent-primary);
  --bs-btn-hover-border-color: var(--accent-primary);
  --bs-btn-hover-color: var(--surface);
  --bs-btn-active-bg: var(--accent-primary);
  --bs-btn-active-border-color: var(--accent-primary);
  --bs-btn-active-color: var(--surface);
}

.btn-outline-secondary {
  --bs-btn-color: var(--text);
  --bs-btn-border-color: var(--border);
  --bs-btn-hover-bg: var(--bg);
  --bs-btn-hover-border-color: var(--accent-primary);
  --bs-btn-hover-color: var(--text);
}

/* Tonal: a way in that is always there (the navbar's finder), so it must
   never outshout the page's one filled action (Von Restorff,
   `docs/conventions.md`). The soft brand fill keeps it a button. On its own
   page (`.active`) it takes the selected chip's teal edge -- "you are here" --
   and still no fill, since the page's own Next is the filled one. */
.btn-tonal {
  --bs-btn-bg: var(--color-brand-soft);
  --bs-btn-border-color: var(--color-brand-soft);
  --bs-btn-color: var(--color-brand);
  --bs-btn-hover-bg: color-mix(in srgb, var(--color-brand) 16%, var(--surface));
  --bs-btn-hover-border-color: color-mix(in srgb, var(--color-brand) 16%, var(--surface));
  --bs-btn-hover-color: var(--color-brand-hover);
  --bs-btn-active-bg: var(--color-brand-soft);
  --bs-btn-active-border-color: var(--accent-primary);
  --bs-btn-active-color: var(--color-brand);
}

.form-control,
.form-select {
  background-color: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

.form-control:focus,
.form-select:focus {
  background-color: var(--surface);
  border-color: var(--accent-primary);
  box-shadow: none;
  color: var(--text);
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

/* "Continue with Telegram" (`docs/telegram-login.md`): the field's height,
   so it reads as the first way in on the sign-in pages and the sheets.
   `.btn.` so the touch-screen 44px floor for every `.btn`, later in this
   file, does not shrink it back. */
.btn.btn-telegram,
.btn.btn-outline-telegram {
  align-items: center;
  display: inline-flex;
  font-weight: 600;
  gap: 0.5rem;
  justify-content: center;
  min-height: var(--field-height);
}

.btn.btn-telegram {
  --bs-btn-bg: var(--color-telegram);
  --bs-btn-border-color: var(--color-telegram);
  --bs-btn-color: var(--color-on-brand);
  --bs-btn-hover-bg: var(--color-telegram-hover);
  --bs-btn-hover-border-color: var(--color-telegram-hover);
  --bs-btn-hover-color: var(--color-on-brand);
  --bs-btn-active-bg: var(--color-telegram-hover);
  --bs-btn-active-border-color: var(--color-telegram-hover);
  --bs-btn-active-color: var(--color-on-brand);
}

/* The same way in where it is the alternative, not the action: the request
   sheet, whose own form and Send request are already on screen. One filled
   button per screen (Von Restorff, `docs/conventions.md`). */
.btn.btn-outline-telegram {
  --bs-btn-bg: var(--color-surface);
  --bs-btn-border-color: var(--color-telegram);
  --bs-btn-color: var(--color-telegram);
  --bs-btn-hover-bg: var(--color-telegram);
  --bs-btn-hover-border-color: var(--color-telegram);
  --bs-btn-hover-color: var(--color-on-brand);
  --bs-btn-active-bg: var(--color-telegram-hover);
  --bs-btn-active-border-color: var(--color-telegram-hover);
  --bs-btn-active-color: var(--color-on-brand);
}

.btn-telegram .fa-telegram,
.btn-outline-telegram .fa-telegram {
  font-size: 1.25rem;
}

.telegram-login__note {
  color: var(--text-muted);
  font-size: var(--text-sm);
  margin: 0.5rem 0 0;
  text-align: center;
}

/* The Telegram mark heading "Connect Telegram" (`telegram_bot/connect.html`). */
.telegram-mark {
  color: var(--color-telegram);
}

/* "or with email": a line either side of a short word, between two ways in. */
.or-divider {
  align-items: center;
  color: var(--text-muted);
  display: flex;
  font-size: var(--text-sm);
  gap: 0.75rem;
  margin: 1.25rem 0;
}

.or-divider::before,
.or-divider::after {
  border-top: var(--bs-border-width) solid var(--border);
  content: "";
  flex: 1 1 auto;
}

.asteriskField {
  color: var(--danger);
  font-weight: 700;
  margin-left: 0.25rem;
}

.crm-conditional-required :is(.form-label, .form-floating > label)::after {
  color: var(--danger);
  content: " *";
  font-weight: 700;
}

/* Fields (`docs/conventions.md`, "Fields"). A field whose label can sit
   inside the box is Bootstrap's floating field (`templates/bootstrap5/
   field.html`): 56px, rounded, the label resting inside like a placeholder
   and floating to the top edge on focus or once filled, so it never
   disappears. Bootstrap does the floating, the autofill case and reduced
   motion; this sets the size and colours and fits the controls it leaves
   out -- the phone prefix, Tom Select, an empty select. */
.form-floating > .form-control,
.form-floating > .form-select {
  border-color: var(--field-border);
  border-radius: var(--field-radius);
  height: var(--field-height);
  min-height: var(--field-height);
  padding-inline: var(--field-pad-x);
}

/* The value line under a floated label, fitted to 56px (Bootstrap's own
   numbers assume 58). */
.form-floating > .form-control:focus,
.form-floating > .form-control:not(:placeholder-shown),
.form-floating > .form-control:-webkit-autofill,
.form-floating > .form-select,
.form-floating.field-static > .form-control {
  padding-bottom: 0.5rem;
  padding-top: 1.5rem;
}

/* Muted at 5.5:1, not Bootstrap's 65% opacity; the accent while typing,
   so the field in use is plain from its label too. */
.form-floating > label {
  color: var(--text-muted);
  padding-inline: var(--field-pad-x);
  transition: transform var(--motion-fast) var(--motion-ease);
}

.form-floating > .form-control:focus ~ label,
.form-floating > .form-select:focus ~ label {
  color: var(--accent-primary);
}

.form-floating.field-static > label {
  transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}

/* A placeholder is only ever an example ("90 123 45 67"), shown under the
   floated label while the field waits for it -- never a second label. */
.form-floating > .form-control:focus::placeholder,
.form-floating.field-static > .form-control::placeholder {
  color: var(--text-muted);
}

/* Errors line up with the text in the box. */
.form-floating ~ .invalid-feedback {
  padding-inline: var(--field-pad-x);
}

.form-floating > .form-control.is-invalid {
  background-position: right var(--field-pad-x) center;
  padding-inline-end: calc(var(--field-pad-x) + 1.5em);
}

.form-floating > .form-select {
  background-position: right var(--field-pad-x) center;
  padding-inline-end: calc(var(--field-pad-x) + 1.75rem);
}

.form-floating > .form-select.is-invalid {
  background-position: right var(--field-pad-x) center, center right 2.75rem;
  padding-inline-end: calc(var(--field-pad-x) + 3.75rem);
}

/* An empty select rests like an empty input: label in the middle, no
   "- Select an option -" under it. Bootstrap floats a select's label always;
   without `:has` that is what stays, which still reads. */
.form-floating.field-select > .form-select:has(option[value=""]:checked):not(:focus) {
  color: transparent;
}

.form-floating.field-select > .form-select:has(option[value=""]:checked):not(:focus) ~ label {
  transform: none;
}

.form-floating.field-select > .form-select option {
  color: var(--text);
}

/* A textarea grows with `rows`; Bootstrap's patch behind the floated label
   takes the field's white, not the page's grey. */
.form-floating > textarea.form-control {
  height: auto;
  min-height: 7.5rem;
}

.form-floating > textarea.form-control ~ label::after {
  background-color: var(--surface);
}

/* The phone: the +998 prefix sits on the value line inside the same box,
   and the label stays up above both ("Phone / +998 90 123 45 67"). */
.uz-phone-prefix {
  white-space: nowrap;
}

.form-floating.field-prefixed > .uz-phone-prefix {
  color: var(--text);
  inset-block-start: calc(1.5rem + var(--bs-border-width));
  inset-inline-start: calc(var(--field-pad-x) + var(--bs-border-width));
  line-height: 1.25;
  pointer-events: none;
  position: absolute;
  z-index: 3;
}

.form-floating.field-prefixed > .form-control {
  padding-inline-start: calc(var(--field-pad-x) + 4.25rem);
}

/* Tom Select hides the select and draws its own box after it, carrying the
   select's classes, so the box is the bordered `.form-select`. Chips wrap
   and grow it under the label; an empty one rests like an empty select. */
.form-floating.field-ts > .ts-wrapper {
  height: auto;
  min-height: var(--field-height);
}

.form-floating.field-ts > .ts-wrapper .ts-control {
  border-radius: inherit;
  min-height: calc(var(--field-height) - 2 * var(--bs-border-width));
  padding-bottom: 0.5rem;
  padding-inline-start: var(--field-pad-x);
  padding-top: 1.5rem;
}

.form-floating.field-ts > .ts-wrapper.disabled {
  background-color: var(--bs-secondary-bg);
}

.form-floating.field-ts > .ts-wrapper.disabled .ts-control {
  background: transparent;
}

.form-floating.field-ts > .ts-wrapper.focus {
  border-color: var(--accent-primary);
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

.form-floating.field-ts > .ts-wrapper.focus .ts-control {
  box-shadow: none;
}

.form-floating.field-ts > .ts-wrapper.focus ~ label {
  color: var(--accent-primary);
}

.form-floating.field-ts > .ts-wrapper:not(.has-items):not(.focus) ~ label {
  transform: none;
}

/* Its "Choose a course" is an example like any placeholder: shown only
   while the field is in use, never beside the chips or the label. */
.form-floating.field-ts > .ts-wrapper:not(.focus) .ts-control input::placeholder {
  color: transparent;
}

/* A box to tick is a 56px tile in the same border, so a form reads as one
   column of same-size boxes. Two classes, to outrank the phone `.form-check`
   row below. */
.form-check.form-check-tile {
  align-items: center;
  background-color: var(--surface);
  border: var(--bs-border-width) solid var(--field-border);
  border-radius: var(--field-radius);
  column-gap: 0.75rem;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  margin-bottom: 0;
  min-height: var(--field-height);
  padding: 0.5rem var(--field-pad-x);
}

.form-check-tile > .form-check-input {
  float: none;
  grid-row: 1;
  margin: 0;
}

.form-check-tile > :not(.form-check-input) {
  grid-column: 2;
}

.form-check-tile > .form-text,
.form-check-tile > .invalid-feedback {
  margin-top: 0;
}

/* What can't float (an editor, an upload, a list of boxes) keeps its label
   above, set like a floated one. */
[id^="div_"] > .form-label {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* A heading right above already names the field ("Photo" over "Profile
   photo"): the label stays for screen readers, not twice on screen. */
.field-label-hidden > [id^="div_"] > .form-label {
  border: 0;
  clip-path: inset(50%);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* Tips (`docs/conventions.md`, "Tips"). The ⓘ after a term or a field's
   label (`core/templatetags/hints.py`): muted, the accent while its bubble
   is open. A 24px mark that leaves the line height alone, grown to a real
   44px target where a finger aims (Fitts's Law; `scripts/mobile_check.mjs`
   measures the button, so the target is the button, not a pseudo-element).
   Link, button and focus styles are the shared ones above. */
/* Positioned, so its visually hidden name and live region stay inside it:
   inside a sideways scroller (the lead board on a phone) they would
   otherwise widen the page. */
.hint {
  display: inline-flex;
  margin-inline-start: var(--space-1);
  position: relative;
  vertical-align: middle;
}

.hint-toggle {
  align-items: center;
  background: none;
  border: 0;
  border-radius: var(--radius-pill);
  color: var(--text-muted);
  display: inline-flex;
  font-size: var(--text-sm);
  height: 1.5rem;
  justify-content: center;
  margin-block: calc(-1 * var(--space-1));
  padding: 0;
  width: 1.5rem;
}

.hint-toggle:hover,
.hint-toggle[aria-expanded="true"] {
  color: var(--accent-primary);
}

@media (max-width: 991.98px), (pointer: coarse) {
  .hint-toggle {
    height: 2.75rem;
    margin: -0.625rem;
    width: 2.75rem;
  }
}

/* A floating field's ⓘ sits inside the box at its end, always the same
   44px spot; the text, the label, a select's caret and the invalid mark all
   stop short of it. */
.form-floating.has-hint {
  --field-suffix: 2.75rem;
}

.form-floating > .hint {
  inset-block-start: calc((var(--field-height) - 2.75rem) / 2);
  inset-inline-end: var(--space-1);
  margin: 0;
  position: absolute;
  z-index: 4;
}

.form-floating > .hint > .hint-toggle {
  height: 2.75rem;
  margin: 0;
  width: 2.75rem;
}

.form-floating.has-hint > label {
  padding-inline-end: calc(var(--field-pad-x) + var(--field-suffix));
}

.form-floating.has-hint > .form-control {
  padding-inline-end: calc(var(--field-pad-x) + var(--field-suffix));
}

.form-floating.has-hint > .form-control.is-invalid {
  background-position: right calc(var(--field-pad-x) + var(--field-suffix)) center;
  padding-inline-end: calc(var(--field-pad-x) + var(--field-suffix) + 1.5em);
}

.form-floating.has-hint > .form-select {
  background-position: right calc(var(--field-pad-x) + var(--field-suffix)) center;
  padding-inline-end: calc(var(--field-pad-x) + var(--field-suffix) + 1.75rem);
}

.form-floating.has-hint > .form-select.is-invalid {
  background-position:
    right calc(var(--field-pad-x) + var(--field-suffix)) center,
    center right calc(2.75rem + var(--field-suffix));
  padding-inline-end: calc(var(--field-pad-x) + var(--field-suffix) + 3.75rem);
}

/* A box to tick: the ⓘ closes the tile's row, after the label. */
.form-check-tile.has-hint {
  grid-template-columns: auto minmax(0, 1fr) auto;
}

.form-check-tile.has-hint > .hint {
  grid-column: 3;
  grid-row: 1;
  margin: 0;
}

/* A list of boxes: the ⓘ sits beside its legend. Bootstrap's reboot floats
   a legend at full width and clears whatever follows it. */
legend.has-hint {
  width: auto;
}

legend.has-hint + .hint {
  clear: none;
  float: left;
  margin-block-start: var(--space-1);
}

legend.has-hint + .hint + * {
  clear: left;
}

/* The bubble floats over the page like a menu: surface, shadow, no border,
   no arrow. An overview's lines break where its text does. */
.popover.hint-bubble {
  --bs-popover-bg: var(--surface);
  --bs-popover-body-color: var(--text);
  --bs-popover-body-padding-x: var(--space-3);
  --bs-popover-body-padding-y: var(--space-2);
  --bs-popover-border-radius: var(--radius-md);
  --bs-popover-border-width: 0;
  --bs-popover-font-size: var(--text-sm);
  --bs-popover-max-width: 18rem;
  box-shadow: var(--shadow-lg);
  white-space: pre-line;
}

/* An icon-only control's name, on hover or keyboard focus (`data-tip`). */
.tooltip {
  --bs-tooltip-bg: var(--text);
  --bs-tooltip-border-radius: var(--radius-sm);
  --bs-tooltip-color: var(--surface);
  --bs-tooltip-font-size: var(--text-xs);
  --bs-tooltip-opacity: 1;
}

/* A menu floats over the page, so its edge is a shadow, not a border: one
   cue per edge (`docs/aesthetic.md`, round 3). */
.dropdown-menu {
  --bs-dropdown-border-width: 0;
  box-shadow: var(--shadow-lg);
}

.dropdown-menu,
.list-group-item,
.offcanvas-lg {
  --bs-dropdown-bg: var(--surface);
  --bs-dropdown-border-color: var(--border);
  --bs-dropdown-link-color: var(--text);
  --bs-dropdown-link-hover-bg: var(--bg);
  --bs-dropdown-link-hover-color: var(--accent-primary);
  --bs-list-group-bg: var(--surface);
  --bs-list-group-color: var(--text);
  --bs-list-group-border-color: var(--border);
  --bs-offcanvas-bg: var(--surface);
  --bs-offcanvas-color: var(--text);
}

.dropdown-item:hover,
.dropdown-item:focus {
  background: var(--bg);
  color: var(--accent-primary);
}

.dropdown-item.active,
.dropdown-item:active,
.nav-pills .nav-link.active {
  background: var(--accent-primary);
  color: var(--surface);
}

/* A card is a white surface on the grey page and nothing more: no outline,
   and so no line under its header or over its footer either (Bootstrap draws
   both from the same width). Space and the surface do the separating
   (`docs/aesthetic.md`, round 3). */
.card {
  --bs-card-bg: var(--surface);
  --bs-card-border-radius: var(--radius-card);
  --bs-card-border-width: 0;
  --bs-card-inner-border-radius: var(--radius-card);
  --bs-card-color: var(--text);
  --bs-card-border-color: var(--border);
  --bs-card-cap-bg: var(--surface);
  --bs-card-cap-color: var(--text);
}

/* A modal floats, so it is a white surface with a shadow; its header and
   footer are set apart by space, not by lines. */
.modal {
  --bs-modal-bg: var(--color-surface);
  --bs-modal-border-width: 0;
  --bs-modal-header-border-width: 0;
  --bs-modal-footer-border-width: 0;
}

.table {
  --bs-table-bg: var(--surface);
  --bs-table-color: var(--text);
  --bs-table-border-color: var(--border);
  --bs-table-striped-bg: var(--bg);
  --bs-table-hover-bg: var(--bg);
}

.badge.text-bg-secondary {
  background: var(--bg) !important;
  color: var(--text) !important;
}

.portal-navbar-inner {
  gap: 1rem;
}

/* The language switch, on every surface. */
.portal-navbar-language .dropdown-toggle,
.portal-navbar--shell .navbar-nav .btn {
  align-items: center;
  display: inline-flex;
  gap: 0.25rem;
  justify-content: center;
  min-height: 2.5rem;
  min-width: 2.5rem;
}

/* The CRM and dashboard bar: one row at every width, like an app's top bar
   -- menu, wordmark, then bell, account and language. It is not an
   expanding navbar, so Bootstrap's collapsed-list defaults (a column, static
   dropdowns, full-width buttons) are undone here. */
.portal-navbar--shell .portal-navbar-inner {
  flex-wrap: nowrap;
  gap: 0.75rem;
}

.portal-navbar--shell .portal-navbar-brand img {
  height: 2.25rem;
}

.portal-navbar--shell .portal-navbar-actions--shell {
  align-items: center;
  flex: 0 0 auto;
  flex-direction: row;
  gap: 0.5rem;
}

.portal-navbar--shell .portal-navbar-actions--shell .nav-item {
  width: auto;
}

.portal-navbar--shell .navbar-nav .dropdown-menu {
  position: absolute;
  right: 0;
  left: auto;
}

.portal-dashboard-menu-button {
  align-items: center;
  display: inline-flex;
  flex: 0 0 auto;
  height: 2.5rem;
  justify-content: center;
  width: 2.5rem;
}

@media (max-width: 575.98px) {
  .portal-navbar--shell .portal-navbar-brand img {
    height: 2rem;
  }

  /* The wordmark's letters give way before the controls do. */
  .portal-navbar--shell .portal-navbar-brand {
    margin-inline-end: auto;
    min-width: 0;
    overflow: hidden;
  }
}

/* Sections and actions keep their width; the search between them grows. */
.portal-navbar-primary,
.portal-navbar-actions {
  flex: 0 0 auto;
  min-width: 0;
}

.portal-navbar-brand {
  flex: 0 0 auto;
}

.portal-navbar-primary {
  justify-content: flex-start;
}

.portal-navbar-actions {
  justify-content: flex-end;
}

/* Notifications: the navbar bell, its menu, and the inbox rows. */
.notification-menu {
  max-width: calc(100vw - 2rem);
  width: 22rem;
}

.notification-menu-list {
  max-height: 24rem;
  overflow-y: auto;
}

.notification-menu-item {
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.75rem 1rem;
  white-space: normal;
}

.notification-menu-item.is-unread,
.notification-row.is-unread {
  background: var(--color-brand-soft);
}

.notification-menu-item.is-unread .notification-title,
.notification-row.is-unread .notification-title {
  font-weight: 700;
}

.notification-title {
  color: var(--text);
}

/* Chunking (Miller's Law, `docs/conventions.md`): related facts, fields or
   links sit in a `.chunk` under a `.group-label`. Groups are set apart by
   space and, where they share one card, a hairline; never by a new card
   per group. One pattern on every surface, so a label always looks like a
   label. */
.group-label,
.chunk > legend {
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.3;
  margin: 0 0 0.5rem;
  text-transform: uppercase;
}

.chunk + .chunk {
  margin-top: var(--chunk-gap);
}

/* Bootstrap's reboot already strips the fieldset; its legend still floats
   at heading size, so it is reset to read as the label it is. */
legend.group-label,
.chunk > legend {
  float: none;
  padding: 0;
  width: auto;
}

.notification-body {
  color: var(--text-muted);
  font-size: 0.875rem;
}

.notification-time {
  color: var(--text-muted);
  font-size: 0.75rem;
}

.notification-count {
  font-size: 0.65rem;
}

.active-filters {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.filter-clear-all {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

.filter-chips {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* One definition: portal.css used to redefine it with different values, and
   since both surfaces load portal.css, those were the values that showed. */
.filter-chip {
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--text);
  display: inline-flex;
  font-size: var(--text-xs);
  gap: 0.5rem;
  max-width: 100%;
  min-height: 2.25rem;
  padding: 0.25rem 0.75rem;
}

.filter-chip:hover,
.filter-chip:focus,
.filter-clear-all:hover,
.filter-clear-all:focus {
  border-color: var(--accent-primary);
  color: var(--accent-primary);
}

.detail-list {
  display: grid;
  gap: 1rem 2rem;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}

.detail-list > div {
  min-width: 0;
}

/* The label is quiet -- smaller and muted, regular weight -- and the value
   carries the weight, so a fact reads as one thing, not two bold ones. */
.detail-list dt {
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: 400;
  margin-bottom: var(--space-1);
}

.detail-list dd {
  font-weight: 700;
  margin-bottom: 0;
  overflow-wrap: anywhere;
}

.portal-footer a {
  color: var(--text);
  display: inline-flex;
  font-size: var(--text-md);
  line-height: 1.6;
}

.portal-footer a:hover,
.portal-footer a:focus {
  color: var(--accent-primary);
}

.footer-heading {
  color: var(--text);
  font-size: var(--text-md);
  font-weight: 700;
  margin-bottom: 0.75rem;
}

/* The wordmark and footer are shared by every surface (see _base.html), so
   their rules live here rather than in portal.css. */
.portal-footer {
  padding-block: 3.5rem 1.5rem;
}

.portal-navbar-brand img,
.footer-wordmark img {
  display: block;
  height: 2.5rem;
  width: auto;
}

/* Every column open at every width (NN/g): two up on a phone, the brand
   across the top; one row from lg. */
.portal-footer-grid {
  display: grid;
  gap: 2rem 1.5rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  padding-bottom: 2.5rem;
}

.footer-brand {
  grid-column: 1 / -1;
}

@media (min-width: 992px) {
  .portal-footer-grid {
    grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr));
  }

  .portal-footer-grid--catalog {
    grid-template-columns: minmax(0, 1.4fr) repeat(4, minmax(0, 1fr));
  }

  .footer-brand {
    grid-column: auto;
  }
}

.footer-tagline {
  color: var(--color-text-muted);
  margin-block: 1rem;
  max-width: 22rem;
}

.footer-brand .footer-socials {
  margin-top: 1rem;
}

.footer-contact-list a,
.portal-footer nav a,
.portal-footer-bottom a {
  color: var(--color-text-muted);
  text-decoration: none;
}

/* Icon and address on one centred line with a real gap: an inline icon rode
   above the text's middle and leant on its first letter. The icon follows the
   link's colour, so a hover reads as one control. */
.footer-contact-list a {
  align-items: center;
  gap: 0.5rem;
}

.footer-contact-list .fact-icon {
  color: inherit;
  margin-inline-end: 0;
}

.footer-contact-list li + li {
  margin-top: 0.25rem;
}

.footer-contact-list a:hover,
.footer-contact-list a:focus-visible,
.portal-footer nav a:hover,
.portal-footer nav a:focus-visible,
.portal-footer-bottom a:hover,
.portal-footer-bottom a:focus-visible {
  color: var(--color-brand);
  text-decoration: underline;
}

.footer-socials {
  display: flex;
  gap: 0.75rem;
}

.footer-socials a {
  align-items: center;
  background: var(--color-background);
  border-radius: 0.7rem;
  color: var(--color-brand);
  display: inline-flex;
  font-size: 1.1rem;
  height: 2.75rem;
  justify-content: center;
  width: 2.75rem;
}

.footer-socials a:hover,
.footer-socials a:focus-visible {
  background: var(--color-brand);
  color: var(--color-on-brand);
}

.portal-footer li + li {
  margin-top: 0.75rem;
}

.portal-footer-bottom {
  align-items: center;
  color: var(--color-text-muted);
  display: flex;
  font-size: var(--text-sm);
  gap: 1rem;
  justify-content: space-between;
  padding-top: 1.5rem;
}

@media (max-width: 575.98px) {
  .portal-footer-bottom {
    align-items: flex-start;
    flex-direction: column;
  }
}

@media (min-width: 992px) {
  .portal-navbar-primary,
  .portal-navbar-actions {
    position: relative;
    z-index: 3;
  }
}

@media (max-width: 991.98px) {
  .portal-navbar-collapse {
    padding-block-start: 1rem;
  }

  /* Inside the public menu only; the CRM bar keeps its row. */
  .portal-navbar-collapse .portal-navbar-primary,
  .portal-navbar-collapse .portal-navbar-actions {
    align-items: stretch;
  }

  .portal-navbar-collapse .portal-navbar-actions {
    gap: 0.5rem;
  }

  .portal-navbar-collapse .portal-navbar-actions .nav-item,
  .portal-navbar-collapse .portal-navbar-primary .nav-item {
    width: 100%;
  }

  .portal-navbar-collapse .portal-navbar-actions .btn,
  .portal-navbar-collapse .portal-navbar-actions .nav-link,
  .portal-navbar-collapse .portal-navbar-primary .nav-link {
    justify-content: flex-start;
    width: 100%;
  }

  .portal-navbar-collapse .portal-navbar-actions .dropdown-menu {
    width: 100%;
  }

}

.ts-wrapper {
  width: 100%;
}

.ts-control,
.ts-dropdown {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

.ts-control input,
.ts-dropdown .option {
  color: var(--text);
}

.ts-dropdown .active {
  background: color-mix(in srgb, var(--accent-primary) 14%, var(--surface));
  color: var(--text);
}

.ts-wrapper.focus .ts-control {
  border-color: var(--accent-primary);
  box-shadow: 0 0 0 0.25rem color-mix(in srgb, var(--accent-primary) 25%, transparent);
}

.ts-wrapper:has(select.is-invalid) .ts-control {
  border-color: var(--danger);
}
.min-w-0 {
  min-width: 0;
}

[x-cloak] {
  display: none !important;
}

/* Breadcrumbs, portal and CRM: a quiet line of links, not a heading. The
   include leaves Home off deep trails and hides the current page, so what
   shows is the way back. */
.breadcrumb {
  --bs-breadcrumb-divider: "›";
  --bs-breadcrumb-divider-color: var(--text-muted);
  --bs-breadcrumb-item-padding-x: 0.5rem;
  font-size: var(--text-sm);
}

.breadcrumb a {
  color: var(--text-muted);
  text-decoration: none;
}

.breadcrumb a:hover,
.breadcrumb a:focus {
  color: var(--accent-primary);
}

/* Trail on the left, share control on the right. */
.page-breadcrumb-row {
  align-items: center;
  display: flex;
  gap: 1rem;
  justify-content: space-between;
  margin-bottom: 1rem;
  min-height: 2rem;
}

.page-breadcrumb-row > .breadcrumb {
  min-width: 0;
}

.page-breadcrumb-row .page-share-button {
  align-items: center;
  border-color: transparent;
  color: var(--text-muted);
  display: inline-flex;
  flex: 0 0 auto;
  height: 2rem;
  justify-content: center;
  padding: 0;
  width: 2rem;
}

.page-breadcrumb-row .page-share-button:hover,
.page-breadcrumb-row .page-share-button:focus-visible,
.page-share-button[data-share-state="done"] {
  background: transparent;
  border-color: transparent;
  color: var(--accent-primary);
}

/* Flash messages render on every surface, sign-in pages included, which do
   not load portal.css. */
.messages-shell,
.messages-shell .alert {
  max-width: none;
  width: 100%;
}

/* Alerts, everywhere: a tint per level and nothing else -- no outline, no
   resting shadow (one cue per edge, `docs/aesthetic.md`, round 3). The tints
   are strong enough to read on the grey page and on a white card alike. */
.alert {
  --bs-alert-bg: var(--color-surface);
  --bs-alert-border-color: transparent;
  --bs-alert-color: var(--color-text);
  border-radius: var(--radius-card);
}

.alert-success,
.alert-info {
  --bs-alert-bg: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface));
}

.alert-warning {
  --bs-alert-bg: var(--identity-yellow-end);
}

.alert-danger {
  --bs-alert-bg: var(--identity-red-end);
}

.messages-shell .alert {
  align-items: flex-start;
  display: flex;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}

/* One icon per level, on the first line of text. */
.messages-shell .alert-icon {
  color: var(--color-brand);
  flex: 0 0 auto;
  font-size: 1.125rem;
  line-height: 1.5rem;
}

.messages-shell .alert-text {
  min-width: 0;
}

.messages-shell .alert-warning .alert-icon {
  color: var(--identity-yellow-ink);
}

.messages-shell .alert-danger .alert-icon {
  color: var(--identity-red-ink);
}

/* Bootstrap pads a dismissible alert's close button by 1.25rem 1rem on top of
   its own size; with the 44px target below that put the × a line too low. No
   padding, and the 44px square centred on the first line of text (1rem of
   alert padding plus half a 1.5rem line, less half the target). */
.messages-shell .alert-dismissible .btn-close {
  padding: 0;
  right: 0.25rem;
  top: 0.375rem;
}

.messages-shell .alert:last-child {
  margin-bottom: 0;
}

/* Fitts's Law, on every surface: below lg, or on any touch screen, a control
   a finger aims at is at least 44px tall (Apple HIG; Material asks 48dp).
   `scripts/mobile_check.mjs` lists anything smaller. */
/* iOS zooms the page into any field under 16px when it is focused, and the
   page stays zoomed after; a small field is 16px on a touch screen. */
@media (pointer: coarse) {
  .form-control-sm,
  .form-select-sm {
    font-size: 1rem;
  }
}

@media (max-width: 991.98px), (pointer: coarse) {
  .btn,
  .form-control,
  .form-select,
  .navbar-toggler,
  .filter-chip,
  .nav-tabs .nav-link,
  .portal-navbar--shell .navbar-nav .btn,
  .portal-dashboard-menu-button {
    min-height: 2.75rem;
  }

  .portal-dashboard-menu-button,
  .portal-navbar--shell .navbar-nav .btn {
    min-width: 2.75rem;
  }

  .filter-chip {
    align-items: center;
    display: inline-flex;
    justify-content: center;
    min-width: 2.75rem;
  }
}

/* The same rule for standalone links and the small chrome around them:
   footer and breadcrumb links, list rows, the drawer's section toggles,
   links in stacked CRM tables, and every close button. The visible text does
   not change; the target around it grows to 44px. */
.btn-close {
  background-position: center;
  background-size: 1em;
  height: 2.75rem;
  padding: 0;
  width: 2.75rem;
}

@media (max-width: 991.98px), (pointer: coarse) {
  .portal-footer nav a,
  .footer-contact-list a,
  .portal-footer-bottom a,
  .breadcrumb a,
  .crm-stack-table td a:not(.btn),
  .dashboard-sidebar .accordion-button,
  .dashboard-sidebar .nav-link,
  .list-group-item-action,
  .portal-navbar-brand {
    align-items: center;
    display: inline-flex;
    min-height: 2.75rem;
  }

  .list-group-item-action,
  .dashboard-sidebar .accordion-button,
  .dashboard-sidebar .nav-link {
    display: flex;
  }

  /* The row height is the spacing now; stacked margins would double it.
     Each footer link is a full-width row, so "IT" is as easy to hit as
     "Finance and accounting". */
  .portal-footer li + li {
    margin-top: 0;
  }

  .ts-wrapper .ts-control {
    min-height: 2.75rem;
  }

  .dropdown-item {
    align-items: center;
    display: flex;
    min-height: 2.75rem;
  }

  .navbar a.navbar-brand,
  a.footer-wordmark {
    align-items: center;
    display: inline-flex;
    min-height: 2.75rem;
  }

  /* The × on a chosen item removes it; it gets a full target of its own. */
  .ts-wrapper.plugin-remove_button .item .remove {
    align-items: center;
    display: inline-flex;
    justify-content: center;
    min-height: 2.75rem;
    min-width: 2.75rem;
  }

  /* A checkbox row is one 44px target: the box and its label together. */
  .form-check {
    align-items: center;
    display: flex;
    gap: 0.5rem;
    min-height: 2.75rem;
    padding-inline-start: 0;
  }

  .form-check .form-check-input {
    flex: 0 0 auto;
    float: none;
    height: 1.25rem;
    margin: 0;
    width: 1.25rem;
  }

  .form-check-label {
    align-items: center;
    display: flex;
    flex: 1 1 auto;
    min-height: 2.75rem;
  }

  .form-switch .form-check-input {
    width: 2.25rem;
  }

  /* Cancel and a destructive confirm, 12px apart. */
  .modal-footer {
    gap: 0.75rem;
  }

  .modal-footer > * {
    margin: 0;
  }

  .portal-footer nav a {
    width: 100%;
  }

  .breadcrumb a,
  .feed-rail-footer a {
    justify-content: center;
    min-width: 2.75rem;
  }

  /* A link that stands on its own line: a 44px row around its text. */
  .touch-link,
  .marketplace-card-chips a {
    align-items: center;
    display: inline-flex;
    min-height: 2.75rem;
  }

  .center-action-rail h2 a {
    align-items: center;
    display: inline-flex;
    min-height: 2.75rem;
  }
}

/* A labelled button that drops its label on phones keeps a 44px square. */
.btn-touch {
  align-items: center;
  display: inline-flex;
  justify-content: center;
  min-width: 2.75rem;
}

/* Spacing (Fitts): 8px between any two targets, 12px around a destructive
   one, so a slip lands on nothing rather than on Delete. */
.dashboard-page-actions,
.crm-row-actions > div,
.schedule-card-actions {
  gap: 0.75rem !important;
}

/* Loose icon pairs (edit, then delete) sit a word-space apart; the delete
   one steps further away. Delete always comes last in its group. */
.btn + .btn-outline-danger.btn-icon,
.btn + .btn-danger.btn-icon {
  margin-inline-start: 0.5rem;
}

/* Peak–End moments -----------------------------------------------------------
   People judge a flow by its best moment and its last one (Peak–End Rule), and
   a designed one reads as a working one (Aesthetic–Usability Effect;
   `docs/aesthetic.md`). Three moments may move, once each: the "Request sent"
   mark, the finder's results arriving, and the first-time welcome. Nothing
   else animates -- not lists, filters or finder steps, which re-swap on every
   change (`docs/ux.md`, "Trade-offs").

   Every duration is a multiple of `--motion-base`, which `tokens.css` sets to
   0s under `prefers-reduced-motion`, so the moments arrive already in place
   for anyone who asked for less motion. Besides these three, only loading
   loops ("Waiting", below); a new keyframe is argued for in
   `docs/aesthetic.md` first. */

/* A result's mark: an icon in a soft brand circle, above its heading rather
   than inside it. `--muted` for a stop that is not a success (a cancelled or
   expired step), which should not celebrate. */
.success-mark {
  align-items: center;
  background: var(--color-brand-soft);
  border-radius: var(--radius-pill);
  block-size: 4rem;
  color: var(--color-brand);
  display: inline-flex;
  flex: 0 0 auto;
  font-size: 1.75rem;
  inline-size: 4rem;
  justify-content: center;
}

.success-mark--muted {
  background: var(--color-surface-strong);
  color: var(--color-text-muted);
}

/* The peak itself is filled: it sits on a modal, whose background is the
   page grey the soft tint disappears into, and it is the one mark in a flow
   that should be seen before anything is read. */
.success-mark--pop {
  animation: peak-pop calc(var(--motion-base) * 1.6) var(--motion-ease) both;
  background: var(--color-brand);
  color: var(--color-on-brand);
}

.success-mark--pop i {
  animation: peak-check calc(var(--motion-base) * 1.2) var(--motion-ease) calc(var(--motion-base) * 0.8) both;
}

/* Rises in once with the page that is the moment (finder results, the
   welcome panel); htmx swaps render without the class. */
.peak-enter {
  animation: peak-rise calc(var(--motion-base) * 1.6) var(--motion-ease) both;
}

@keyframes peak-pop {
  from {
    opacity: 0;
    transform: scale(0.6);
  }

  70% {
    opacity: 1;
    transform: scale(1.08);
  }

  to {
    transform: scale(1);
  }
}

@keyframes peak-check {
  from {
    opacity: 0;
    transform: scale(0.4) rotate(-20deg);
  }

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

@keyframes peak-rise {
  from {
    opacity: 0;
    transform: translateY(0.75rem);
  }

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

/* "Requested": every button that opens a request sheet ships both labels, so
   the public page stays the same for every visitor (`docs/cache.md`), and
   `portal.js` marks the groups this visitor has already asked about. Here
   rather than in `portal.css` because CRM pages render the same schedule
   partials. */
.requested-label {
  display: none;
}

[data-requested] .request-label {
  display: none;
}

[data-requested] .requested-label {
  display: inline;
}

.btn[data-requested]:disabled {
  background: var(--color-brand-soft);
  border-color: var(--color-brand-soft);
  color: var(--color-brand);
  opacity: 1;
}

/* Waiting (Doherty Threshold, `docs/conventions.md`) -------------------------
   Every wait answers within 400 ms: the pressed control at once, the page bar
   from 300 ms, a real percentage for an upload, and words when it fails
   (`static/js/waiting.js`). Nothing is ever held back to look busier. */

/* htmx's own indicator styles are switched off (`htmx-config` in base.html):
   they carried a literal 200ms the reduced-motion tokens could not reach. An
   indicator appears only once a wait passes `--motion-base`, so a fast answer
   never flashes a spinner; it disappears at once. */
.htmx-indicator {
  opacity: 0;
  visibility: hidden;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
  transition: opacity var(--motion-fast) var(--motion-ease) var(--motion-base);
  visibility: visible;
}

/* A busy control dims at once and stops taking presses (`waiting.js` also
   swallows a keyboard one). If the wait passes `--motion-base`, `waiting.js`
   sets `data-busy="spin"` and a ring in the button's own text colour takes
   the label's place, so nothing shifts. Bootstrap's own `spinner-border`
   keyframe, so this adds no new one. */
.btn[data-busy] {
  cursor: progress;
  opacity: var(--loading-opacity);
  pointer-events: none;
  position: relative;
}

.btn[data-busy="spin"],
.btn[data-busy="spin"]:hover,
.btn[data-busy="spin"]:focus-visible {
  color: transparent;
}

/* Opacity, not visibility: an icon-only button's name is a visually hidden
   span, which must stay in the accessibility tree while it waits. */
.btn[data-busy="spin"] > * {
  opacity: 0;
}

.btn[data-busy="spin"]::after {
  animation: spinner-border 0.75s linear infinite;
  border: 0.15em solid var(--bs-btn-color);
  border-inline-end-color: transparent;
  border-radius: 50%;
  content: "";
  height: 1em;
  inset: 0;
  margin: auto;
  position: absolute;
  width: 1em;
}

/* The page bar: one thin line at the very top for any wait past 300 ms. Its
   fill is an estimate that creeps towards 90% and jumps to the end when the
   answer lands; it never shows at all for a faster one. */
.page-progress {
  inset-inline: 0;
  height: 3px;
  opacity: 0;
  pointer-events: none;
  position: fixed;
  top: 0;
  transition: opacity var(--motion-fast) var(--motion-ease);
  z-index: 1095;
}

.page-progress.is-active {
  opacity: 1;
}

.page-progress > span {
  background: var(--color-brand);
  display: block;
  height: 100%;
  transform: scaleX(var(--progress, 0));
  transform-origin: left center;
  transition: transform var(--motion-base) var(--motion-ease);
}

.page-progress.is-resetting > span {
  transition: none;
}

/* A failed request says so: one notice at the bottom of the screen, above the
   phone's tab bar and any open modal, styled as a flash message. */
.request-notice {
  bottom: calc(var(--space-4) + var(--request-notice-offset, 0px) + env(safe-area-inset-bottom));
  inset-inline: var(--space-4);
  margin-inline: auto;
  max-width: 32rem;
  position: fixed;
  z-index: 1090;
}

.request-notice .alert {
  box-shadow: var(--shadow-lg);
  margin: 0;
}

/* Skeletons (`docs/conventions.md`, "Doherty Threshold"): the component's
   own shape, from `templates/includes/skeletons/`, inside the component's
   own classes, so it takes the space the content will take and the swap
   moves nothing. The one looping animation besides the busy ring; reduced
   motion stops it. */
:root {
  --placeholder-fill: color-mix(in srgb, var(--border) 75%, var(--surface));
  --skeleton-fill: linear-gradient(
    90deg,
    var(--placeholder-fill) 25%,
    color-mix(in srgb, var(--accent-primary) 18%, var(--surface)) 45%,
    var(--placeholder-fill) 65%
  );
}

.skeleton {
  animation: skeleton-pulse 1.2s ease-in-out infinite;
  background: var(--skeleton-fill);
  background-size: 240% 100%;
  border-radius: var(--radius-pill);
  display: block;
}

/* One line of the text it stands for: exactly as tall as its parent's line,
   with a bar the height of the letters, so a skeleton title is a title's
   height in any type style. */
.skeleton-text {
  display: block;
  height: 1lh;
  position: relative;
}

.skeleton-text::before {
  animation: skeleton-pulse 1.2s ease-in-out infinite;
  background: var(--skeleton-fill);
  background-size: 240% 100%;
  border-radius: var(--radius-pill);
  content: "";
  inset-block: calc((1lh - 0.75em) / 2);
  inset-inline: 0;
  position: absolute;
}

/* A line's height and nothing to see: the label inside a skeleton button. */
.skeleton-hold {
  display: block;
  height: 1lh;
}

/* Fills the real image or avatar box it sits in. */
.skeleton-fill {
  border-radius: inherit;
  height: 100%;
  width: 100%;
}

/* A real button's box, filled: its padding, border and line, no label. */
.btn.skeleton-button {
  animation: skeleton-pulse 1.2s ease-in-out infinite;
  background: var(--skeleton-fill);
  background-size: 240% 100%;
  border-color: transparent;
  min-width: 6rem;
  pointer-events: none;
}

/* A sheet's main action is its widest ("Send request", "Save", "Delete"). */
.modal-footer > :is(.btn-primary, .btn-danger).skeleton-button {
  min-width: 9rem;
}

.skeleton-line {
  height: 0.75rem;
}

.skeleton-field {
  border-radius: var(--field-radius, var(--radius-md));
  height: var(--field-height, 3.5rem);
}

/* A textarea's resting height (`base.css`, "Fields"). */
.skeleton-field--textarea {
  height: 7.5rem;
}

/* Lines and fields stacked as a form or a card will stack them. */
.skeleton-stack {
  display: grid;
  gap: var(--space-3);
}

/* A photo's box shows where the picture will be until it paints, as a
   feed's does: the skeleton's resting tone, still, behind an opaque photo
   that then covers it. Not logos, which may be transparent and would keep
   the tone for good. */
.feed-attachment-thumb,
.reel-cover,
.center-teacher-photo,
img.profile-avatar--round,
.crm-detail-image,
.rich-text-content figure img,
.article-body figure img {
  background-color: var(--placeholder-fill);
}

/* A sheet that differs for a guest and a signed-in visitor (the request and
   rating sheets) takes the shape the `kvm` hint says this browser will get. */
.is-member .skeleton-guest,
:root:not(.is-member) .skeleton-member {
  display: none;
}

@keyframes skeleton-pulse {
  from {
    background-position: 120% 0;
  }

  to {
    background-position: -120% 0;
  }
}

/* A zeroed duration on an `infinite` animation is not the same as stopping
   it, so the two loops stop here: the ring stands still, the skeleton is a
   flat shape. */
@media (prefers-reduced-motion: reduce) {
  .btn[data-busy]::after,
  .skeleton,
  .skeleton-text::before,
  .btn.skeleton-button {
    animation: none;
  }
}
