/* MVTEACHES design system — modern SaaS dashboard direction.
   UI-only pass (see CLAUDE.md): every class below already exists in the
   Razor markup (_Layout.cshtml, Admin/Teacher/Guardian/Student pages).
   Nothing here changes markup, model binding, validation, or business
   rules — only how the existing structure is styled. */

/* tokens/base */
:root {
  --mv-bg: #f8fafc;
  --mv-surface: #ffffff;
  --mv-surface-muted: #f1f5f9;
  --mv-text: #0f172a;
  --mv-muted: #556070;
  --mv-border: #e2e8f0;
  --mv-primary: #2554c7;
  --mv-primary-strong: #16337e;
  --mv-primary-soft: #eaf1ff;
  --mv-accent: #7c3aed;
  --mv-accent-soft: #f2e9ff;
  --mv-success: #15803d;
  --mv-success-soft: #eafaf0;
  --mv-warning: #b45309;
  --mv-warning-soft: #fff7e6;
  --mv-danger: #c0261e;
  --mv-danger-soft: #fdedec;
  --mv-focus: #3b82f6;
  --mv-radius-sm: 0.55rem;
  --mv-radius: 0.9rem;
  --mv-radius-lg: 1.25rem;
  --mv-radius-xl: 1.75rem;
  --mv-shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05);
  --mv-shadow-sm: 0 2px 10px rgba(15, 23, 42, 0.06);
  --mv-shadow: 0 16px 40px -12px rgba(15, 23, 42, 0.16), 0 4px 14px -6px rgba(15, 23, 42, 0.08);
  --mv-shadow-lg: 0 28px 64px -16px rgba(15, 23, 42, 0.22);
  /* ⭐ Owner request 2026-09-19: one colour per navigation family, for the
     sidebar ICONS only — the labels stay --mv-text throughout. Restrained on
     purpose: these are desaturated siblings of the palette above, not a
     rainbow, and several families deliberately share a hue because they are
     the same kind of work.

     Measured rather than assumed: as declared, every value clears 4.5:1 on
     --mv-surface (worst 4.92); as actually rendered, at the 0.85 idle opacity
     the tone rules apply, the worst case is 3.73:1 — above the 3:1 WCAG 1.4.11
     asks of a non-text graphic, and these icons are aria-hidden decoration
     beside a text label in any case. Redeclared once under [data-theme="dark"]
     (worst 5.57:1 composited); nothing else needs to know they exist. */
  --mv-tone-dashboard: #2554c7;      /* primary blue — where the day starts */
  --mv-tone-students: #0e7490;       /* cyan — people the centre teaches */
  --mv-tone-teachers: #7c3aed;       /* accent purple — people who teach */
  --mv-tone-packages: #4f46e5;       /* indigo — what a family buys */
  --mv-tone-payments: #15803d;       /* green — money that arrived */
  --mv-tone-promo: #b45309;          /* warm amber — marketing and discounts */
  --mv-tone-schedules: #0f766e;      /* teal — time and lessons */
  --mv-tone-compensation: #c2410c;   /* orange — a family is owed something */
  --mv-tone-reports: #15803d;        /* green — the money view, same family */
  --mv-tone-certificates: #a16207;   /* restrained gold — an award */
  --mv-tone-administration: #475569; /* slate — the machinery, not the work */

  --mv-empty-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23556070' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 13h4l2 3h6l2-3h4'/%3E%3Cpath d='M5 13V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v7'/%3E%3Cpath d='M3 13v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4'/%3E%3C/svg%3E");
  --mv-sidebar: 18rem;
  --mv-topbar: 4.5rem;
  --bs-body-font-family: "Almarai", "Segoe UI", "Tahoma", "Arial", sans-serif;
  --bs-body-color: var(--mv-text);
  /* Bootstrap's OWN rgb-triplet twin of --bs-body-color, used by its rgba()
     opacity tricks (e.g. the floating-label colour once a field is focused
     or filled — see the dark-mode override below for why this one matters). */
  --bs-body-color-rgb: 15, 23, 42;
  --bs-body-bg: var(--mv-bg);
  --bs-primary: var(--mv-primary);
  --bs-primary-rgb: 37, 84, 199;
  --bs-border-color: var(--mv-border);
  --bs-border-radius: var(--mv-radius);
}

html {
  min-height: 100%;
  font-size: 16px;
}

body {
  min-height: 100%;
  margin: 0;
  background:
    radial-gradient(60rem 30rem at 90% -10%, rgba(124, 58, 237, 0.07), transparent 60%),
    radial-gradient(50rem 26rem at -10% 0%, rgba(37, 84, 199, 0.08), transparent 55%),
    var(--mv-bg);
  color: var(--mv-text);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--mv-primary);
  text-underline-offset: 0.18em;
  transition: color 150ms ease;
}

a:hover {
  color: var(--mv-accent);
}

:focus-visible {
  outline: 3px solid var(--mv-focus);
  outline-offset: 2px;
  border-radius: 0.25rem;
}

/* ⭐ Accessibility 2026-09-19 (WCAG 2.4.1): the keyboard escape hatch past the
   sidebar. Off-screen rather than display:none or visibility:hidden, because
   both of those remove it from the focus order entirely — which would make it
   a link nobody can ever reach. Uses inset-inline-start so it lands on the
   reading-side edge in Arabic and English alike. */
.app-skip-link {
  position: fixed;
  inset-block-start: 0.75rem;
  inset-inline-start: 0.75rem;
  z-index: 2000;
  padding: 0.7rem 1.1rem;
  border-radius: var(--mv-radius-sm);
  background: var(--mv-surface);
  color: var(--mv-primary-strong);
  font-weight: 700;
  text-decoration: none;
  box-shadow: var(--mv-shadow);
  transform: translateY(-160%);
}

.app-skip-link:focus-visible {
  transform: translateY(0);
}

/* The landmark is only focusable so the link above can land on it; it must
   never draw a focus ring when a mouse click happens to land on the page. */
.app-main:focus {
  outline: none;
}

/* layout/navigation */
.app-shell {
  min-height: 100vh;
}

.app-topbar {
  position: sticky;
  inset-block-start: 0;
  z-index: 1030;
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: var(--mv-topbar);
  padding: 0.75rem clamp(1rem, 2vw, 2rem);
  border-block-end: 1px solid var(--mv-border);
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(16px) saturate(1.4);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  box-shadow: var(--mv-shadow-xs);
}

.app-brand {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
}

.app-brand-mark {
  display: inline-grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: var(--mv-radius);
  background: linear-gradient(150deg, var(--mv-primary), var(--mv-primary-strong));
  color: #fff;
  font-weight: 800;
  text-decoration: none;
  box-shadow: 0 6px 16px -4px rgba(37, 84, 199, 0.45);
}

.app-brand-name {
  display: block;
  color: var(--mv-text);
  font-weight: 800;
  line-height: 1.1;
  text-decoration: none;
}

.app-brand-subtitle,
.app-eyebrow {
  color: var(--mv-muted);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}

.app-eyebrow {
  display: inline-block;
  padding: 0.3rem 0.75rem;
  border-radius: 999px;
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
  margin-block-end: 0.75rem;
}

.app-topbar-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
}

.app-sidebar {
  position: fixed;
  inset-block-start: var(--mv-topbar);
  inset-block-end: 0;
  inset-inline-start: 0;
  width: var(--mv-sidebar);
  padding: 1.25rem 1rem;
  overflow-y: auto;
  border-inline-end: 1px solid var(--mv-border);
  background: var(--mv-surface);
}

.app-main {
  min-height: calc(100vh - var(--mv-topbar));
  margin-inline-start: var(--mv-sidebar);
  padding: clamp(1.25rem, 2.4vw, 2.25rem);
}

.app-main.public {
  --public-pad: clamp(1rem, 3vw, 3rem);
  margin-inline-start: 0;
  padding: var(--public-pad);
}

.app-page-heading {
  margin-block-end: 1.25rem;
}

.app-page-heading h1,
.home-hero-content h1 {
  margin: 0;
  color: var(--mv-primary-strong);
  font-weight: 800;
  letter-spacing: 0;
}

.app-main > h1,
.app-main > h2,
.app-main > .h1,
.app-main > .h2 {
  color: var(--mv-primary-strong);
  font-weight: 800;
  letter-spacing: 0;
}

.app-main > h1 {
  margin-block: 0 0.5rem;
  font-size: clamp(1.7rem, 2.2vw, 2.35rem);
}

.app-main > h2,
.app-main > .h2 {
  margin-block-start: 1.75rem;
}

.app-main > p.text-muted:first-of-type {
  margin-block-end: 1.35rem;
}

.app-page-heading h1 {
  font-size: clamp(1.7rem, 2.2vw, 2.35rem);
}

.app-nav {
  display: grid;
  gap: 0.2rem;
}

.app-nav-group {
  margin-block-start: 1.1rem;
  padding-inline: 0.75rem;
  color: var(--mv-muted);
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* The section you are standing in. Owner report 2026-09-05: pages with no nav
   link of their own (Reschedule, StudentDetails, Payroll) left the sidebar
   silent about where you were. */
.app-nav-group.is-active {
  color: var(--mv-primary-strong);
}

/* The current page named under its section, when it has no link of its own.
   Deliberately not a link - it says where you are, it does not navigate. */
.app-nav-link.is-here {
  cursor: default;
  font-weight: 700;
  color: var(--mv-primary-strong);
  background: var(--mv-primary-soft);
}

.app-nav-link {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  min-height: 2.75rem;
  padding: 0.6rem 0.85rem;
  border-radius: var(--mv-radius-sm);
  border-inline-start: 3px solid transparent;
  color: var(--mv-text);
  font-weight: 650;
  text-decoration: none;
  transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
}

.app-nav-link:hover {
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
}

.app-nav-link.active {
  border-inline-start-color: var(--mv-accent);
  background: linear-gradient(90deg, var(--mv-primary-soft), rgba(234, 241, 255, 0));
  color: var(--mv-primary-strong);
  font-weight: 800;
}

.app-nav-icon {
  width: 0.5rem;
  height: 0.5rem;
  flex: 0 0 0.5rem;
  border-radius: 999px;
  background: currentColor;
  opacity: 0.65;
}

.app-nav-link.active .app-nav-icon {
  background: var(--mv-accent);
  opacity: 1;
}

.app-menu-button {
  display: none;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  background: var(--mv-surface);
}

.app-menu-button span {
  display: block;
  width: 1.2rem;
  height: 2px;
  margin: 0.25rem auto;
  border-radius: 999px;
  background: var(--mv-primary);
}

.app-language-switch {
  display: inline-flex;
  padding: 0.2rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  background: var(--mv-surface-muted);
}

.app-language-switch button {
  min-height: 2.1rem;
  padding: 0.2rem 0.75rem;
  border: 0;
  border-radius: calc(var(--mv-radius-sm) - 0.2rem);
  background: transparent;
  color: var(--mv-muted);
  font-weight: 700;
  transition: background-color 150ms ease, color 150ms ease, box-shadow 150ms ease;
}

.app-language-switch button.active {
  background: var(--mv-surface);
  color: var(--mv-primary);
  box-shadow: var(--mv-shadow-xs);
}

.app-theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  flex: 0 0 auto;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  background: var(--mv-surface-muted);
  color: var(--mv-muted);
  cursor: pointer;
  transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
}

.app-theme-toggle:hover {
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
  border-color: var(--mv-primary);
}

.app-theme-icon {
  width: 1.15rem;
  height: 1.15rem;
  stroke: currentColor;
  stroke-width: 1.8;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Both icons stay in the DOM always; only display swaps with the theme, so
   there is nothing for the server to compute and nothing that can mismatch
   the localStorage-driven attribute set before paint (see _Layout.cshtml). */
.app-theme-icon.is-moon {
  display: none;
}

.app-user-menu {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
}

.app-user-avatar {
  display: inline-grid;
  place-items: center;
  width: 2.15rem;
  height: 2.15rem;
  border-radius: 999px;
  background: linear-gradient(150deg, var(--mv-accent), #5b21b6);
  color: #fff;
  font-weight: 800;
  box-shadow: 0 4px 12px -3px rgba(124, 58, 237, 0.5);
}

.app-user-name {
  max-width: 16rem;
  overflow: hidden;
  color: var(--mv-muted);
  font-size: 0.9rem;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* components */
.card,
.alert,
.list-group,
.dropdown-menu,
.offcanvas {
  border-color: var(--mv-border);
  border-radius: var(--mv-radius-lg);
}

.card {
  --bs-card-spacer-x: 1.35rem;
  --bs-card-spacer-y: 1.25rem;
  border: 1px solid var(--mv-border);
  box-shadow: var(--mv-shadow-sm);
  transition: transform 200ms ease, box-shadow 200ms ease;
}

.card:hover {
  transform: translateY(-2px);
  box-shadow: var(--mv-shadow);
}

.card-header {
  background: var(--mv-surface-muted);
  border-bottom: 1px solid var(--mv-border);
  font-weight: 800;
}

.card-title {
  color: var(--mv-primary-strong);
  font-weight: 800;
}

.btn {
  min-height: 2.75rem;
  padding-inline: 1.15rem;
  border-radius: var(--mv-radius-sm);
  font-weight: 700;
  transition: transform 150ms ease, box-shadow 150ms ease, background-color 150ms ease, border-color 150ms ease;
}

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

.btn-sm {
  min-height: 2.5rem;
  padding-inline: 0.85rem;
}

.btn-lg {
  min-height: 3.25rem;
  padding-inline: 1.5rem;
}

.btn-primary {
  border-color: var(--mv-primary);
  background: linear-gradient(150deg, var(--mv-primary), var(--mv-primary-strong));
  box-shadow: 0 8px 20px -8px rgba(37, 84, 199, 0.55);
}

.btn-primary:hover {
  border-color: var(--mv-primary-strong);
  background: linear-gradient(150deg, var(--mv-primary-strong), var(--mv-primary-strong));
  box-shadow: 0 10px 24px -6px rgba(22, 51, 126, 0.55);
}

.btn-outline-secondary {
  border-color: var(--mv-border);
  color: var(--mv-primary-strong);
  background: var(--mv-surface);
}

.btn-outline-secondary:hover {
  border-color: var(--mv-primary);
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
}

.badge {
  border-radius: 999px;
  padding: 0.4em 0.75em;
  font-weight: 700;
}

.alert {
  border-width: 1px;
  border-radius: var(--mv-radius);
}

.alert-info {
  border-color: #b9d9ec;
  background: #edf7fc;
  color: #124663;
}

.alert-success {
  border-color: #bfe6cd;
  background: var(--mv-success-soft);
  color: var(--mv-success);
}

.alert-warning {
  border-color: #f3d49b;
  background: var(--mv-warning-soft);
  color: var(--mv-warning);
}

.alert-danger,
.text-danger {
  color: var(--mv-danger) !important;
}

/* ⭐ 2026-09-17: Bootstrap's own .text-warning is #ffc107 - 1.6:1 on white,
   and it is how this app prints its inline warnings ("nothing suitable is
   scheduled", "empty until you press Gather", a basic Zoom plan...). Same
   treatment .text-danger already had: the theme's status tokens, which are
   legible in light AND dark mode. .text-success is aligned with it. */
.text-warning {
  color: var(--mv-warning) !important;
}

.text-success {
  color: var(--mv-success) !important;
}

.alert-danger {
  border-color: #f3c6c2;
  background: var(--mv-danger-soft);
}

/* forms */
.form-control,
.form-select,
.form-check-input,
textarea {
  border-color: var(--mv-border);
  border-radius: var(--mv-radius-sm);
  transition: border-color 150ms ease, box-shadow 150ms ease;
}

.form-control,
.form-select {
  min-height: 2.75rem;
}

.form-control:focus,
.form-select:focus,
.form-check-input:focus,
.btn:focus-visible {
  border-color: var(--mv-focus);
  box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.22);
}

.form-floating > label {
  color: var(--mv-muted);
  font-weight: 600;
}

label,
.form-label {
  color: var(--mv-text);
  font-weight: 700;
}

.validation-summary-errors,
.field-validation-error {
  color: var(--mv-danger);
}

/* tables */
.table-responsive {
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius);
  background: var(--mv-surface);
  box-shadow: var(--mv-shadow-sm);
}

.table {
  margin-block-end: 0;
  vertical-align: middle;
}

.table thead th {
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  border-block-end: 1px solid var(--mv-border);
  background: var(--mv-surface-muted);
  color: var(--mv-primary-strong);
  font-size: 0.82rem;
  font-weight: 800;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.table tbody tr {
  transition: background-color 150ms ease;
}

/* ⭐ 2026-09-19: :focus-within alongside :hover. The row highlight was the
   only thing saying which record you were about to act on, and it existed
   for the mouse alone - tab into a row's Edit button and nothing moved, on
   tables whose action columns are identical from row to row. Keyboard and
   pointer now get the same answer to the same question. */
.table tbody tr:hover,
.table tbody tr:focus-within {
  background: var(--mv-primary-soft);
}

.table td,
.table th {
  padding: 0.85rem 0.9rem;
}

/* dashboards/public */
.row {
  --bs-gutter-x: 1.25rem;
  --bs-gutter-y: 1.25rem;
}

.display-6 {
  color: var(--mv-primary-strong);
  font-weight: 800;
}

.text-muted {
  color: var(--mv-muted) !important;
}

/* ⭐ Owner request 2026-09-15: a genuinely full-screen photo hero — breaks
   out of .app-main.public's own padding by negating that exact padding
   value (--public-pad, set on .app-main.public above) rather than the
   classic 100vw trick, which counts the scrollbar in some browsers and
   causes horizontal overflow (confirmed live: it broke the sticky topbar).
   Negating the real padding var reaches precisely the same edges with no
   viewport-unit arithmetic, and needs no separate RTL rule since it is
   symmetric. While hasHeroImage is false (see Index.cshtml) this still
   renders — same content, just without the photo/scrim/white-text
   treatment, which is what has-photo alone gates. */
.home-hero {
  position: relative;
  margin-inline: calc(-1 * var(--public-pad, 0px));
  margin-block-start: calc(-1 * var(--public-pad, 0px));
  min-height: calc(100vh - var(--mv-topbar));
  display: flex;
  align-items: flex-end;  /* anchor content to bottom */
  justify-content: center;
  overflow: hidden;
}

/* ⭐ Owner request 2026-09-17: the photo must be seen WHOLE - its top and
   bottom were being cut off by object-fit: cover. It is now contained (never
   stretched, never cropped), and whatever the viewport's shape leaves
   uncovered is filled by the same image blurred and dimmed behind it, so the
   hero stays full-screen with no flat letterbox bars. Hard-coded dark tones,
   not --mv-* tokens: this is a photo treatment and reads the same in light
   and dark mode. */
/* Dark ground under the photo heroes for the moment before the (large)
   photo has loaded, so the white copy on top is never white-on-white. */
.home-hero.has-photo,
.auth-hero.has-photo {
  background-color: #0b1220;
}

.home-hero-backdrop,
.auth-hero-backdrop {
  position: absolute;
  inset: -2rem;
  inline-size: calc(100% + 4rem);
  block-size: calc(100% + 4rem);
  object-fit: cover;
  filter: blur(28px) brightness(0.55) saturate(1.15);
  transform: scale(1.06);
}

.home-hero-photo {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: fill;
  object-position: center;
}

.home-hero-scrim {
  position: absolute;
  inset: 0;
  /* Rich cinematic gradient: very dark at bottom where text lives,
     fades upward so the photo still reads as the dominant element */
  background:
    linear-gradient(0deg,
      rgba(4, 8, 18, 0.92) 0%,
      rgba(4, 8, 18, 0.62) 35%,
      rgba(4, 8, 18, 0.22) 65%,
      rgba(4, 8, 18, 0.08) 100%),
    radial-gradient(ellipse 80% 60% at 40% 100%, rgba(79, 40, 175, 0.35), transparent 70%);
}

.home-hero-content {
  position: relative;
  width: 100%;
  max-width: 62rem;
  padding: clamp(2.5rem, 6vw, 5rem) clamp(1.5rem, 5vw, 5rem);
  isolation: isolate;
  z-index: 1;
  /* Anchor text to the bottom-start for editorial impact */
  align-self: flex-end;
  margin-block-end: clamp(2.5rem, 6vh, 5rem);
}

/* No card, no box — text floats directly on the photo */
.home-hero.has-photo .home-hero-content {
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  text-align: start;
}

/* ⭐ Owner request 2026-09-17: the headline carries the page. Same words,
   stronger treatment - a controlled brand gradient clipped to the letters and
   a short accent rule under them. Light surface: deep brand blue into the
   violet accent. On the photo: white warming into the gold already used for
   the sign-in page's headline, lifted off the image with a soft drop shadow
   (text-shadow would show through clipped text, filter does not). */
.home-hero-content h1 {
  max-width: 17ch;
  padding-block-end: 0.12em;
  font-size: clamp(2.2rem, min(4.8vw, 7.5vh), 4.1rem);
  line-height: 1.12;
  text-wrap: balance;
}

[dir="rtl"] .home-hero-content h1 {
  line-height: 1.32;
}

.home-hero-content h1::after {
  content: "";
  display: block;
  inline-size: 4.5rem;
  block-size: 0.3rem;
  margin-block-start: 0.55em;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--mv-primary), var(--mv-accent));
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  /* Top to bottom, not across: a horizontal gradient restarts on every
     wrapped line and lands its warm end on whichever word happens to end a
     line, which reads as accidental. Vertically, the headline warms line by
     line in reading order in both Arabic and English. */
  .home-hero-content h1 {
    background: linear-gradient(180deg, var(--mv-primary-strong) 0%, var(--mv-primary) 55%, var(--mv-accent) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }

  .home-hero.has-photo .home-hero-content h1 {
    background-image: linear-gradient(180deg, #ffffff 0%, #fff4d8 42%, #ffd873 78%, #ffc247 100%);
    filter: drop-shadow(0 2px 16px rgba(0, 0, 0, 0.5));
  }
}

.home-hero-content p {
  max-width: 42rem;
  color: var(--mv-muted);
  font-size: 1.08rem;
}

/* Only when a real photo sits behind the copy does the copy need to turn
   white/scrim-legible — the no-photo fallback keeps the page's normal
   dark-on-light text untouched. .app-eyebrow's own base rule (near the top
   of this file) also paints it a pale blue pill background, which is what
   made it look wrong sitting on a photo — that needed overriding too, not
   just the text colour. */
.home-hero.has-photo .home-hero-content .app-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: #ffd873;
  background: rgba(124, 58, 237, 0.25);
  border: 1px solid rgba(255, 215, 115, 0.35);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  letter-spacing: 0.12em;
  font-size: 0.78rem;
  font-weight: 700;
}

.home-hero.has-photo .home-hero-content h1 {
  color: #fff;
  text-shadow: 0 4px 32px rgba(0, 0, 0, 0.55);
}

/* Only where the gradient cannot be clipped to the text */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .home-hero.has-photo .home-hero-content h1 {
    text-shadow: 0 2px 20px rgba(0, 0, 0, 0.4);
  }
}

.home-hero.has-photo .home-hero-content h1::after {
  background: linear-gradient(90deg, #ffd873, #ffc247);
  inline-size: 3rem;
}

.home-hero.has-photo .home-hero-content p {
  color: rgba(255, 255, 255, 0.85);
  font-size: 1.12rem;
  max-width: 38rem;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}

/* Frosted glass pill for outline button on photo */
.home-hero.has-photo .home-hero-content .btn-outline-secondary {
  border-color: rgba(255, 255, 255, 0.55);
  background: rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: #fff;
}

.home-hero.has-photo .home-hero-content .btn-outline-secondary:hover {
  border-color: rgba(255, 255, 255, 0.9);
  background: rgba(255, 255, 255, 0.22);
  color: #fff;
}

.public-hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  margin-block-start: 2rem;
  justify-content: flex-start;
}

/* Center on no-photo fallback */
.home-hero:not(.has-photo) .public-hero-actions {
  justify-content: center;
}

/* The 4-step workflow, now its own section below the fold. */
.public-workflow {
  padding-block: clamp(2.5rem, 5vw, 4rem);
}

.public-hero-panel {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.85rem;
}

.public-hero-panel > div {
  display: grid;
  grid-template-columns: 3rem 1fr;
  gap: 0.15rem 0.85rem;
  padding: 1.15rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-lg);
  background: rgba(255, 255, 255, 0.86);
  box-shadow: var(--mv-shadow-sm);
  backdrop-filter: blur(6px);
  transition: transform 200ms ease, box-shadow 200ms ease;
}

.public-hero-panel > div:hover {
  transform: translateY(-3px);
  box-shadow: var(--mv-shadow);
}

.public-hero-panel span {
  grid-row: span 2;
  display: inline-grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 999px;
  background: linear-gradient(150deg, var(--mv-accent), #5b21b6);
  color: #fff;
  font-weight: 800;
  box-shadow: 0 6px 16px -4px rgba(124, 58, 237, 0.45);
}

.public-hero-panel strong {
  color: var(--mv-primary-strong);
  font-size: 1.05rem;
}

.public-hero-panel p {
  margin: 0;
  font-size: 0.95rem;
}

.auth-panel {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(20rem, 28rem);
  align-items: center;
  gap: clamp(1.5rem, 4vw, 4rem);
  min-height: calc(100vh - var(--mv-topbar) - 5rem);
}

.auth-copy h1 {
  max-width: 14ch;
  color: var(--mv-primary-strong);
  font-size: clamp(2rem, 4.5vw, 3.75rem);
  font-weight: 800;
  line-height: 1.1;
}

.auth-copy p:not(.app-eyebrow) {
  max-width: 34rem;
  color: var(--mv-muted);
  font-size: 1.05rem;
}

.auth-card {
  padding: clamp(1.25rem, 3vw, 2.25rem);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-xl);
  background: rgba(255, 255, 255, 0.94);
  box-shadow: var(--mv-shadow-lg);
}

.status-panel {
  display: grid;
  grid-template-columns: 4rem minmax(0, 42rem);
  gap: 1rem;
  align-items: start;
  max-width: 52rem;
  padding: clamp(1.25rem, 3vw, 2.25rem);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-lg);
  background: var(--mv-surface);
  box-shadow: var(--mv-shadow-sm);
}

.status-icon {
  width: 3rem;
  height: 3rem;
  border-radius: 999px;
  background: var(--mv-accent-soft);
}

.status-icon.danger {
  background: var(--mv-danger-soft);
  box-shadow: inset 0 0 0 0.7rem #fff, inset 0 0 0 1.5rem var(--mv-danger);
}

/* RTL/responsive overrides */
[dir="rtl"] .form-floating > label {
  right: 0;
  left: auto;
}

[dir="rtl"] .form-floating > .form-control {
  padding-right: 0.75rem;
  padding-left: 0.75rem;
}

@media (max-width: 991.98px) {
  .app-menu-button {
    display: inline-block;
  }

  .app-sidebar {
    display: none;
  }

  .app-main {
    margin-inline-start: 0;
  }

  .app-brand-subtitle,
  .app-user-name {
    display: none;
  }

  .auth-panel {
    grid-template-columns: 1fr;
    align-items: start;
    min-height: auto;
  }

  .home-hero-content {
    max-width: none;
  }

  /* ⭐ 2026-09-17: a contained full-screen photo on a portrait screen is a
     thin strip lost in blur, so narrow screens get the whole photo as a
     banner at its own proportions with the copy below it, still on the
     blurred backdrop. The bottom scrim is not needed once text no longer
     sits on the photo itself. */
  .home-hero.has-photo {
    flex-direction: column;
    align-items: stretch;
    min-height: 0;
  }

  .home-hero.has-photo .home-hero-photo {
    position: relative;
    block-size: auto;
    max-block-size: 70vh;
  }

  .home-hero.has-photo .home-hero-scrim {
    display: none;
  }
}

@media (max-width: 575.98px) {
  .app-topbar {
    align-items: flex-start;
    gap: 0.75rem;
  }

  /* ⭐ 2026-09-17: on a phone the brand name, the menu button and the
     topbar actions do not fit on one row, and the name slid underneath the
     menu button (measured: overlapping boxes at 390px). The "MV" mark stays
     and still links home; the written name is dropped at this width only. */
  .app-brand > div {
    display: none;
  }

  .app-topbar-actions {
    flex-wrap: wrap;
    justify-content: flex-end;
  }

  .app-language-switch {
    order: 10;
  }

  .home-hero-content h1 {
    max-width: none;
    font-size: 2.25rem;
  }

  .public-hero-panel > div {
    grid-template-columns: 2.5rem 1fr;
  }

  .card:hover {
    transform: none;
  }

  /* ⭐ Accessibility 2026-09-19. This rule used to be
       .table { display: block; overflow-x: auto; white-space: nowrap; }
     and it did three unwanted things at once on a phone. `display: block`
     strips the table's own role from the accessibility tree in several
     browsers, so exactly where a screen-reader user most needs row/column
     association, they lost it. It also defeated the sticky header built
     further up (a sticky <th> needs a real table row to stick inside), and
     `white-space: nowrap` forced long Arabic names into one endless
     horizontal drag.

     Every table in this app is already inside a .table-responsive wrapper
     (52 of 52), and that wrapper is what scrolls — at this width as at every
     other. So the table keeps being a table, and the min-width below is what
     stops columns collapsing into unreadable slivers instead. */
  .table {
    min-width: 34rem;
  }

  .table-responsive {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* ⭐ Owner request 2026-09-19 — the family half of the mobile story.
     An admin wants the grid and can drag it sideways; a parent checking their
     daughter's next lesson on a phone should not have to. Under this width the
     eight Guardian/Student tables lay each row out as a card, with the column
     name printed beside its own value.

     The roles are the load-bearing part. Changing `display` on a table wipes
     its IMPLICIT roles in several browsers - the exact defect the phone rule
     above was rewritten to stop - so every one of these tables, rows and cells
     states its role EXPLICITLY in the markup. The layout changes; what a
     screen reader announces does not.

     Admin tables are untouched and keep scrolling horizontally. */
  .app-cards-sm {
    min-width: 0;
    display: block;
  }

  .app-cards-sm thead {
    /* Not display:none — that would take the header out of the accessibility
       tree along with the layout. Clipped instead, so it is still announced
       where a browser wants it, while each cell also carries its own label. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .app-cards-sm tbody,
  .app-cards-sm tr,
  .app-cards-sm td {
    display: block;
    width: auto;
  }

  .app-cards-sm tr {
    padding: 0.35rem 0;
    border: 1px solid var(--mv-border);
    border-radius: var(--mv-radius-sm);
    margin-block-end: 0.75rem;
    background: var(--mv-surface);
  }

  .app-cards-sm tbody tr:hover {
    background: var(--mv-surface);
  }

  .app-cards-sm td {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: baseline;
    justify-content: space-between;
    border: 0;
    padding: 0.4rem 0.9rem;
    text-align: start;
  }

  /* The column name, printed once per value. attr() is safe here: the label is
     put there by Razor from the table's own <th>, so the two can never drift. */
  .app-cards-sm td[data-label]::before {
    content: attr(data-label);
    color: var(--mv-muted);
    font-size: 0.8rem;
    font-weight: 700;
    flex: 0 0 auto;
  }

  /* An empty state spans the whole table; it is one message, not a labelled
     value, so it keeps the plain centred treatment. */
  .app-cards-sm td[colspan] {
    display: block;
    text-align: center;
  }
}

/* Local Staging only (Env.IsStaging() in _Layout.cshtml) — a persistent,
   impossible-to-miss reminder that everything on screen is test data, never
   real students/guardians/payments. Sits above the app shell on every page. */
.app-staging-banner {
  background: #b45309;
  color: #fff;
  text-align: center;
  font-weight: 600;
  padding: 0.5rem 1rem;
  font-size: 0.9rem;
  letter-spacing: 0.01em;
}

/* page header, toolbars, and list affordances
   Added with the UI/UX pass: a consistent "what am I looking at, what can I do
   here, how do I find one row" band at the top of every list screen. */
.app-page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-block-end: 1.25rem;
}

.app-page-head p {
  max-width: 70ch;
  margin-block-end: 0;
}

.app-page-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.app-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-block-end: 0.75rem;
}

.app-toolbar .form-control {
  max-width: 22rem;
}

.app-search {
  position: relative;
  flex: 1 1 16rem;
  max-width: 24rem;
}

/* A long <select> rebuilt into a searchable combobox by wireSelectFilters()
   in site.js — the real <select> stays in the DOM (hidden, but still the
   form control that actually submits) directly under the visible search
   input, with the match panel floating below it. */
.app-combo {
  position: relative;
}

.app-combo-native {
  position: absolute;
  inset-inline-start: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.app-combo-input.app-combo-input {
  margin-block-end: 0;
}

.app-combo-panel {
  position: absolute;
  z-index: 20;
  inset-inline-start: 0;
  inset-inline-end: 0;
  top: 100%;
  margin-block-start: 0.25rem;
  max-height: 15rem;
  overflow-y: auto;
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  box-shadow: var(--mv-shadow);
  padding: 0.3rem;
}

.app-combo-option {
  display: block;
  width: 100%;
  text-align: start;
  padding: 0.45rem 0.6rem;
  border: none;
  border-radius: 0.5rem;
  background: transparent;
  color: var(--mv-text);
  font-size: 0.92rem;
  cursor: pointer;
}

.app-combo-option:hover,
.app-combo-option.is-active {
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
}

.app-combo-empty {
  padding: 0.5rem 0.6rem;
  color: var(--mv-muted);
  font-size: 0.88rem;
}

/* A record's own reference (payment reference, certificate number). Internal
   database ids are not shown to users at all — this is for codes a person can
   actually quote back to the centre. */
.app-ref {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 0.4rem;
  background: var(--mv-surface-muted);
  color: var(--mv-muted);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* A "choose which child" list (placement test, package purchase) — used to
   be a bare underlined <a> inside a list item. This makes the whole row the
   click target, styled like a real choice rather than a text link. */
.app-choice-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.85rem 1.1rem;
  color: var(--mv-text);
  text-decoration: none;
  font-weight: 600;
}

.app-choice-link:hover,
.app-choice-link:focus-visible {
  color: var(--mv-primary-strong);
}

.app-choice-arrow {
  color: var(--mv-muted);
  font-size: 1.1rem;
}

[dir="rtl"] .app-choice-arrow {
  display: inline-block;
  transform: scaleX(-1);
}

.app-empty {
  display: grid;
  gap: 0.35rem;
  padding: 1.75rem 1.25rem;
  border: 1px dashed var(--mv-border);
  border-radius: var(--mv-radius);
  background: var(--mv-surface);
  color: var(--mv-muted);
  text-align: center;
}

/* ⭐ Owner request 2026-09-19: an icon on meaningful empty states, and only
   there — not on every table row. One rule reaches all 21 of them with no
   markup change, and it is drawn with the same 1.7-weight round-capped line
   style as the sidebar glyphs so it belongs to the same family. Injected as a
   pseudo-element, so it is decoration a screen reader never announces: the
   heading and the sentence under it carry the meaning, as they already did.

   currentColor in the data URI would not resolve, so the stroke is the muted
   token's own value; the dark-mode twin is declared beside the other dark
   tokens further down. */
.app-empty::before {
  content: "";
  justify-self: center;
  width: 2rem;
  height: 2rem;
  margin-block-end: 0.35rem;
  background: var(--mv-empty-glyph) center / contain no-repeat;
  opacity: 0.7;
}

.app-empty strong {
  color: var(--mv-text);
}

/* status pills — one vocabulary for every state badge in the app */
.app-status {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.7rem;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 800;
  white-space: nowrap;
}

.app-status::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  background: currentColor;
}

.app-status.is-success {
  background: var(--mv-success-soft);
  color: var(--mv-success);
}

.app-status.is-warning {
  background: var(--mv-warning-soft);
  color: var(--mv-warning);
}

.app-status.is-danger {
  background: var(--mv-danger-soft);
  color: var(--mv-danger);
}

.app-status.is-info {
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
}

.app-status.is-muted {
  background: var(--mv-surface-muted);
  color: var(--mv-muted);
}

/* KPI card that is also a link to the screen where the work gets done */
.app-kpi {
  display: block;
  height: 100%;
  padding: 1.25rem 1.35rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-lg);
  background: var(--mv-surface);
  box-shadow: var(--mv-shadow-sm);
  color: inherit;
  text-decoration: none;
  transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
}

a.app-kpi:hover {
  transform: translateY(-2px);
  border-color: var(--mv-primary);
  box-shadow: var(--mv-shadow);
  color: inherit;
}

.app-kpi-label {
  display: block;
  color: var(--mv-muted);
  font-size: 0.85rem;
  font-weight: 700;
}

.app-kpi-value {
  display: block;
  margin-block: 0.35rem 0.15rem;
  color: var(--mv-primary-strong);
  font-size: 2.1rem;
  font-weight: 800;
  line-height: 1.1;
}

.app-kpi-hint {
  display: block;
  color: var(--mv-muted);
  font-size: 0.82rem;
}

.app-kpi.is-attention {
  border-color: #f3d49b;
  background: var(--mv-warning-soft);
}

.app-kpi.is-attention .app-kpi-value {
  color: var(--mv-warning);
}

/* profile summary strip (student/teacher header) */
.app-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  padding: 1.25rem 1.35rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-lg);
  background: var(--mv-surface);
  box-shadow: var(--mv-shadow-sm);
  margin-block-end: 1.25rem;
}

.app-summary > div {
  min-width: 9rem;
}

.app-summary dt {
  color: var(--mv-muted);
  font-size: 0.8rem;
  font-weight: 700;
}

.app-summary dd {
  margin: 0;
  color: var(--mv-text);
  font-size: 1.05rem;
  font-weight: 800;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }

  .card:hover,
  .public-hero-panel > div:hover {
    transform: none !important;
  }
}

/* ===================================================================
   Second UI/UX pass — the pieces the first pass left missing.

   Everything below is presentation for markup that already exists or
   is added in the same pass: a numbered step rail for the screens that
   are genuinely a sequence, a plain-language guide panel so every
   screen answers "what do I do now?", quick-action tiles on the student
   profile, a dependent-field group for the payment-method form, and a
   dependency-free bar chart for the dashboard. No library, no
   framework, no business rule.
   =================================================================== */

/* --- restraint pass on the base ------------------------------------
   Every card lifting on hover reads as a template, not a product. A
   card that is not a link should not pretend to be one. */
.card:hover {
  transform: none;
  box-shadow: var(--mv-shadow-sm);
}

.card {
  border-radius: var(--mv-radius);
}

.card-header {
  background: var(--mv-surface);
  border-bottom: 1px solid var(--mv-border);
}

.table thead th {
  text-transform: none;
  font-size: 0.85rem;
  letter-spacing: 0;
  color: var(--mv-muted);
  font-weight: 700;
}

.table tbody tr:hover,
.table tbody tr:focus-within {
  background: var(--mv-surface-muted);
}

.table td,
.table th {
  padding: 0.8rem 0.9rem;
}

/* --- sidebar: real icons, quieter groups --------------------------- */
.app-nav-icon {
  display: inline-grid;
  place-items: center;
  width: 1.15rem;
  height: 1.15rem;
  flex: 0 0 1.15rem;
  border-radius: 0;
  background: none;
  /* colour and opacity are set by the tone rules a little further down, so
     that a toned and an untoned icon are decided in exactly one place. */
}

.app-nav-icon svg {
  width: 1.15rem;
  height: 1.15rem;
  stroke: currentColor;
  stroke-width: 1.7;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ⭐ Owner request 2026-09-19: semantic icon tones.
   The icon carries a restrained family colour; the LABEL stays one colour on
   every entry, so the strip still reads as one list and not a rainbow.

   Each class sets ONE custom property rather than the colour directly. That is
   what lets the idle, hover and active rules below all say
   `var(--mv-nav-tone, <fallback>)` and get the right answer without any of
   them knowing which family they are painting — and it is why an untoned icon
   (the "you are here" dot) still falls back to exactly what it used before.
   The family values themselves live in :root, redeclared once for dark mode,
   so no hex appears in the markup and both navs share one system: the desktop
   sidebar and the mobile offcanvas render the same .app-nav-icon. */
.app-nav-icon.tone-dashboard { --mv-nav-tone: var(--mv-tone-dashboard); }
.app-nav-icon.tone-students { --mv-nav-tone: var(--mv-tone-students); }
.app-nav-icon.tone-teachers { --mv-nav-tone: var(--mv-tone-teachers); }
.app-nav-icon.tone-packages { --mv-nav-tone: var(--mv-tone-packages); }
.app-nav-icon.tone-payments { --mv-nav-tone: var(--mv-tone-payments); }
.app-nav-icon.tone-promo { --mv-nav-tone: var(--mv-tone-promo); }
.app-nav-icon.tone-schedules { --mv-nav-tone: var(--mv-tone-schedules); }
.app-nav-icon.tone-compensation { --mv-nav-tone: var(--mv-tone-compensation); }
.app-nav-icon.tone-reports { --mv-nav-tone: var(--mv-tone-reports); }
.app-nav-icon.tone-certificates { --mv-nav-tone: var(--mv-tone-certificates); }
.app-nav-icon.tone-administration { --mv-nav-tone: var(--mv-tone-administration); }

/* Resting: the family colour, held a little back so it tints rather than
   competes with the label beside it. */
.app-nav-icon {
  color: var(--mv-nav-tone, var(--mv-muted));
  opacity: 0.85;
}

/* Hover and active still win, and the active row wins hardest: it keeps its
   primary-tinted background and its inline-start bar, which is what the eye
   actually reads as "you are here". The icon only comes up to full strength
   in its own family colour — a tone is a resting state, never a rival signal. */
.app-nav-link:hover .app-nav-icon,
.app-nav-link.active .app-nav-icon {
  color: var(--mv-nav-tone, var(--mv-primary));
  background: none;
  opacity: 1;
}

.app-nav-link.active {
  background: var(--mv-primary-soft);
}

.app-nav-group:first-child {
  margin-block-start: 0;
}

/* --- breadcrumb ---------------------------------------------------- */
.app-crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-block-end: 0.5rem;
  color: var(--mv-muted);
  font-size: 0.85rem;
}

.app-crumbs a {
  color: var(--mv-muted);
  text-decoration: none;
}

.app-crumbs a:hover {
  color: var(--mv-primary);
  text-decoration: underline;
}

.app-crumbs .app-crumb-sep {
  opacity: 0.5;
}

/* --- "what is this screen, and what do I do now?" ------------------
   One calm panel per screen, in the words the reader uses. Never a
   technical message, never a stack of alerts. */
.app-guide {
  display: grid;
  gap: 0.6rem;
  padding: 1.1rem 1.25rem;
  margin-block-end: 1.5rem;
  border: 1px solid var(--mv-border);
  border-inline-start: 3px solid var(--mv-primary);
  border-radius: var(--mv-radius);
  background: linear-gradient(180deg, var(--mv-primary-soft), #fff 65%);
}

.app-guide h2,
.app-guide .app-guide-title {
  margin: 0;
  color: var(--mv-primary-strong);
  font-size: 1rem;
  font-weight: 800;
}

.app-guide p {
  max-width: 78ch;
  margin: 0;
  color: var(--mv-text);
  font-size: 0.94rem;
}

.app-guide ul,
.app-guide ol {
  max-width: 78ch;
  margin: 0;
  padding-inline-start: 1.15rem;
  color: var(--mv-text);
  font-size: 0.94rem;
}

.app-guide li + li {
  margin-block-start: 0.25rem;
}

.app-guide-next {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 800;
  color: var(--mv-primary-strong);
}

/* --- numbered steps ------------------------------------------------
   For the screens that really are a sequence (assisted registration,
   reschedule, payroll). Not decoration: each step says what it is for
   and which ones are already done. */
.app-steps {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 1.25rem;
  padding: 0;
  list-style: none;
  counter-reset: mv-step;
}

.app-steps > li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex: 1 1 12rem;
  min-width: 0;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  background: var(--mv-surface);
  color: var(--mv-muted);
  font-size: 0.9rem;
  font-weight: 700;
}

.app-steps > li::before {
  counter-increment: mv-step;
  content: counter(mv-step);
  display: inline-grid;
  place-items: center;
  flex: 0 0 1.75rem;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  background: var(--mv-surface-muted);
  color: var(--mv-muted);
  font-weight: 800;
}

.app-steps > li.is-current {
  border-color: var(--mv-primary);
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
}

.app-steps > li.is-current::before {
  background: var(--mv-primary);
  color: #fff;
}

.app-steps > li.is-done {
  border-color: #bfe6cd;
  background: var(--mv-success-soft);
  color: var(--mv-success);
}

.app-steps > li.is-done::before {
  content: "\2713";
  background: var(--mv-success);
  color: #fff;
}

.app-step-label {
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.app-step-hint {
  display: block;
  font-size: 0.78rem;
  font-weight: 600;
  opacity: 0.85;
}

/* --- one step of a single-page sequence ---------------------------- */
.app-step-panel {
  position: relative;
  padding: 1.35rem;
  margin-block-end: 1.25rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius);
  background: var(--mv-surface);
  box-shadow: var(--mv-shadow-xs);
}

.app-step-panel > h2 {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-block: 0 0.35rem;
  color: var(--mv-primary-strong);
  font-size: 1.1rem;
  font-weight: 800;
}

.app-step-badge {
  display: inline-grid;
  place-items: center;
  flex: 0 0 1.9rem;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 999px;
  background: var(--mv-primary);
  color: #fff;
  font-size: 0.9rem;
  font-weight: 800;
}

.app-step-panel.is-done {
  background: var(--mv-surface-muted);
  box-shadow: none;
}

.app-step-panel.is-done .app-step-badge {
  background: var(--mv-success);
}

.app-step-panel.is-done > h2::after {
  content: "\2713";
  margin-inline-start: auto;
  color: var(--mv-success);
  font-size: 0.95rem;
  font-weight: 800;
}

.app-step-panel.is-waiting {
  background: var(--mv-surface-muted);
}

.app-step-panel.is-waiting .app-step-badge {
  background: var(--mv-border);
  color: var(--mv-muted);
}

.app-step-panel > p.app-step-lead {
  max-width: 78ch;
  margin-block-end: 1rem;
  color: var(--mv-muted);
  font-size: 0.92rem;
}

/* --- quick actions on a profile ------------------------------------ */
.app-quick-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.65rem;
  margin-block-end: 1.5rem;
}

.app-quick-action {
  display: grid;
  gap: 0.15rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  background: var(--mv-surface);
  color: var(--mv-text);
  text-decoration: none;
  transition: border-color 150ms ease, background-color 150ms ease;
}

.app-quick-action:hover {
  border-color: var(--mv-primary);
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
}

.app-quick-action strong {
  font-size: 0.95rem;
  font-weight: 800;
}

.app-quick-action span {
  color: var(--mv-muted);
  font-size: 0.8rem;
}

.app-quick-action:hover span {
  color: var(--mv-primary-strong);
}

/* --- required-field marker ----------------------------------------- */
.app-required {
  color: var(--mv-danger);
  font-weight: 800;
  margin-inline-start: 0.15rem;
}

.form-text {
  color: var(--mv-muted);
  font-size: 0.82rem;
}

/* --- dependent fields (payment-method form) ------------------------ */
[data-mv-when] {
  transition: opacity 120ms ease;
}

/* --- a dependency-free bar chart -----------------------------------
   Height comes from --mv-bar (0..1) set inline per bar; no library,
   no canvas, and the same numbers are printed as text beside each bar
   so the chart is never the only way to read them. */
.app-chart {
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  height: 9rem;
  padding: 0.75rem 0.5rem 0;
  border-radius: var(--mv-radius-sm);
  background: linear-gradient(180deg, var(--mv-surface-muted), #fff);
}

.app-chart-col {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 0.35rem;
  min-width: 0;
  height: 100%;
}

.app-chart-bar {
  width: 100%;
  max-width: 2.75rem;
  min-height: 0.25rem;
  height: calc(var(--mv-bar, 0) * 100%);
  border-radius: 0.35rem 0.35rem 0 0;
  background: linear-gradient(180deg, var(--mv-primary), var(--mv-primary-strong));
}

.app-chart-col.is-empty .app-chart-bar {
  background: var(--mv-border);
}

.app-chart-value {
  color: var(--mv-primary-strong);
  font-size: 0.85rem;
  font-weight: 800;
}

.app-chart-axis {
  display: flex;
  gap: 0.5rem;
  padding: 0.4rem 0.5rem 0;
}

.app-chart-axis > span {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  color: var(--mv-muted);
  font-size: 0.75rem;
  font-weight: 700;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- a compact "what happens next" line under a form --------------- */
.app-outcome {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.6rem 0.8rem;
  border-radius: var(--mv-radius-sm);
  background: var(--mv-surface-muted);
  color: var(--mv-text);
  font-size: 0.86rem;
}

.app-outcome::before {
  content: "\2192";
  color: var(--mv-primary);
  font-weight: 800;
}

[dir="rtl"] .app-outcome::before {
  content: "\2190";
}

/* --- upcoming-session rows on the dashboard ------------------------ */
.app-feed {
  display: grid;
  gap: 0.5rem;
}

.app-feed-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  background: var(--mv-surface);
}

.app-feed-when {
  min-width: 12rem;
  color: var(--mv-primary-strong);
  font-weight: 800;
}

.app-feed-what {
  flex: 1 1 12rem;
  min-width: 0;
  color: var(--mv-text);
}

.app-feed-meta {
  color: var(--mv-muted);
  font-size: 0.85rem;
}

@media (max-width: 575.98px) {
  .app-steps > li {
    flex: 1 1 100%;
  }

  .app-feed-when {
    min-width: 100%;
  }
}

/* --- Tabbed action panels (Admin > Schedules) ---------------------------
   One screen, four unrelated actions. Bootstrap's own tab JS drives them
   (already loaded via bootstrap.bundle) - this is only the surface, built
   from the same tokens as .card so a tab panel reads as the same object the
   cards used to be, not as a second visual language. */
.app-tabs {
  margin-bottom: 1.5rem;
}

.app-tabs > .nav-tabs {
  border-bottom: 1px solid var(--mv-border);
  gap: 0.25rem;
  flex-wrap: wrap;
}

.app-tabs > .nav-tabs .nav-link {
  border: 1px solid transparent;
  border-bottom: 0;
  border-radius: var(--mv-radius-sm) var(--mv-radius-sm) 0 0;
  color: var(--mv-muted);
  font-weight: 600;
  padding: 0.65rem 1.05rem;
  /* The tab strip is the primary navigation of this screen; it must not
     shift by a pixel when a tab becomes active, so the active border is
     reserved as a transparent one from the start. */
  transition: color 150ms ease, background-color 150ms ease;
}

.app-tabs > .nav-tabs .nav-link:hover {
  background: var(--mv-surface-muted);
  color: var(--mv-text);
}

.app-tabs > .nav-tabs .nav-link:focus-visible {
  outline: 2px solid var(--mv-primary);
  outline-offset: -2px;
}

.app-tabs > .nav-tabs .nav-link.active {
  background: var(--mv-surface);
  border-color: var(--mv-border);
  color: var(--mv-primary-strong);
  box-shadow: inset 0 3px 0 0 var(--mv-primary);
}

.app-tabs > .tab-content {
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-top: 0;
  border-radius: 0 0 var(--mv-radius) var(--mv-radius);
  box-shadow: var(--mv-shadow-sm);
  padding: 1.25rem 1.35rem;
}

.app-tabs > .tab-content > .tab-pane > .card-title {
  margin-bottom: 0.35rem;
}

/* "Not on this screen" - each entry names where the thing actually lives
   today, so the tab is an answer rather than a promise. */
.app-next-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.85rem;
}

.app-next-list > li {
  background: var(--mv-surface-muted);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  padding: 0.85rem 1rem;
  display: grid;
  gap: 0.25rem;
}

.app-next-list > li > strong {
  color: var(--mv-text);
}

.app-next-list > li > span {
  color: var(--mv-muted);
  font-size: 0.9rem;
}

.app-next-list .app-choice-link {
  justify-self: start;
  margin-top: 0.35rem;
}

/* --- Sign-in split-screen entrance --------------------------------------
   The two halves arrive from opposite sides and settle, so the page reads as
   one split surface rather than two boxes that happened to load. CSS only:
   no markup, no script, and nothing on the Login page itself changed - the
   sign-in form, its fields and its posting behaviour are untouched.

   The global (prefers-reduced-motion: reduce) block further down already
   collapses every animation-duration to 0.01ms, so this needs no opt-out of
   its own; it simply appears, which is the correct reduced-motion outcome.

   Deliberately NOT a Sign in <-> Sign up slide: this product has no public
   sign-up page (accounts are created by an admin), so there is no second
   panel to slide to. Adding one would be a change to how people get accounts,
   not a design polish. */
@keyframes mv-auth-rise-start {
  from { opacity: 0; transform: translateX(-1.75rem); }
  to   { opacity: 1; transform: none; }
}

@keyframes mv-auth-rise-end {
  from { opacity: 0; transform: translateX(1.75rem); }
  to   { opacity: 1; transform: none; }
}

.auth-panel > .auth-copy {
  animation: mv-auth-rise-start 620ms cubic-bezier(0.22, 0.68, 0.36, 1) both;
}

.auth-panel > .auth-card {
  /* Slightly behind the copy: the eye lands on the sentence, then on the
     form it is being asked to fill in. */
  animation: mv-auth-rise-end 620ms cubic-bezier(0.22, 0.68, 0.36, 1) 90ms both;
}

/* Arabic reads right-to-left, so "in from the outside" is the mirrored
   direction on both halves. */
[dir="rtl"] .auth-panel > .auth-copy {
  animation-name: mv-auth-rise-end;
}

[dir="rtl"] .auth-panel > .auth-card {
  animation-name: mv-auth-rise-start;
}

/* --- Student profile ----------------------------------------------------
   A person, not a query result: who they are in one band at the top, then
   the handful of numbers an admin asks for before deciding anything, then
   one subject at a time behind tabs. Built from the same tokens as the rest
   of the product - no second visual language for this screen. */
.app-profile-hero {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius);
  box-shadow: var(--mv-shadow-sm);
  padding: 1.15rem 1.35rem;
  margin-bottom: 1.25rem;
}

.app-profile-identity {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-width: 0;
}

.app-profile-identity h1 {
  margin: 0;
  font-size: clamp(1.35rem, 2.4vw, 1.8rem);
  line-height: 1.25;
}

.app-avatar {
  flex: 0 0 auto;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
  font-size: 1.4rem;
  font-weight: 800;
  /* An initial is decoration, not information - the name is right beside it. */
  user-select: none;
}

.app-profile-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0.4rem 0 0;
}

.app-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: var(--mv-surface-muted);
  border: 1px solid var(--mv-border);
  border-radius: 999px;
  padding: 0.15rem 0.65rem;
  font-size: 0.82rem;
  color: var(--mv-muted);
}

.app-chip.is-level {
  background: var(--mv-primary-soft);
  border-color: transparent;
  color: var(--mv-primary-strong);
  font-weight: 700;
}

.app-chip.is-missing {
  background: var(--mv-danger-soft);
  border-color: transparent;
  color: var(--mv-danger);
  font-weight: 700;
}

/* A closed-by-default multi-select: a toggle that says what is chosen, a panel
   that opens over the page, and the picks echoed back as chips underneath.
   Built for the teaching-permission form, where twenty-one courses used to sit
   permanently unrolled. */
.app-multiselect {
  position: relative;
}

.app-multiselect-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  text-align: start;
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm, 8px);
  padding: 0.45rem 0.75rem;
  font-size: 0.9rem;
  color: var(--mv-text);
  cursor: pointer;
}

.app-multiselect-toggle::after {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-inline-end: 2px solid currentColor;
  border-block-end: 2px solid currentColor;
  transform: rotate(45deg) translateY(-25%);
  opacity: 0.55;
  flex: none;
}

.app-multiselect-toggle:hover,
.app-multiselect-toggle:focus-visible {
  border-color: var(--mv-primary);
}

.app-multiselect-toggle[aria-expanded="true"] {
  border-color: var(--mv-primary);
}

.app-multiselect-summary.is-empty,
[data-mv-multiselect-summary].is-empty {
  color: var(--mv-muted);
}

.app-multiselect-panel {
  position: absolute;
  z-index: 30;
  inset-inline: 0;
  margin-top: 0.25rem;
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm, 8px);
  box-shadow: 0 10px 28px rgb(15 23 42 / 12%);
  padding: 0.6rem;
}

.app-multiselect-options {
  max-height: 14rem;
  overflow-y: auto;
}

.app-multiselect-options .form-check {
  padding-block: 0.15rem;
}

/* Where the ticked ones are echoed back, so a closed panel still says what was
   chosen. Empty until something is picked. */
.app-chip-tray {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.app-chip-tray:empty {
  display: none;
}

.app-chip.is-removable {
  background: var(--mv-primary-soft);
  border-color: transparent;
  color: var(--mv-primary-strong);
  font-weight: 600;
  cursor: pointer;
}

.app-chip.is-removable::after {
  content: "×";
  font-weight: 700;
  opacity: 0.7;
}

.app-chip.is-removable:hover,
.app-chip.is-removable:focus-visible {
  background: var(--mv-primary);
  color: #fff;
}

/* A row that can unfold its own controls. Used by the awaiting-confirmation
   list, where the confirm/reject form is wide but is only ever used on one
   payment at a time. */
.app-row-details > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  cursor: pointer;
  list-style: none;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--mv-border);
  border-radius: 999px;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--mv-primary-strong);
  background: var(--mv-primary-soft);
}

.app-row-details > summary::-webkit-details-marker {
  display: none;
}

.app-row-details[open] > summary {
  margin-block-end: 0.5rem;
}

.app-row-details-body {
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm, 8px);
  background: var(--mv-surface-muted);
  padding: 0.75rem;
  min-width: 22rem;
}

/* One line per course, its levels beside it. Keeps a teacher's whole teaching
   permission readable inside a table cell. */
.app-grant-list {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.app-grant-row {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.app-grant-course {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--mv-text);
  min-width: 9rem;
}

.app-grant-levels {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.app-profile-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* --- Stat cards --------------------------------------------------------- */
.app-stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
  gap: 0.85rem;
  margin-bottom: 1.5rem;
}

.app-stat {
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius);
  box-shadow: var(--mv-shadow-xs);
  padding: 0.9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}

/* A card that needs somebody to do something is marked once, on its edge -
   not by recolouring the whole card, which would make five loud cards. */
.app-stat.is-attention {
  border-inline-start: 3px solid var(--mv-warning);
}

.app-stat-label {
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--mv-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.app-stat-value {
  font-size: 1.55rem;
  font-weight: 800;
  line-height: 1.2;
  color: var(--mv-text);
  overflow-wrap: anywhere;
}

.app-stat-value.is-text {
  font-size: 1.05rem;
}

.app-stat-value.is-empty {
  color: var(--mv-muted);
  font-weight: 700;
}

.app-stat-value.is-good {
  color: var(--mv-success);
}

.app-stat-value.is-bad {
  color: var(--mv-danger);
}

.app-stat-note {
  font-size: 0.85rem;
  color: var(--mv-muted);
}

/* --- Progress bar ------------------------------------------------------- */
.app-meter {
  height: 0.45rem;
  border-radius: 999px;
  background: var(--mv-surface-muted);
  overflow: hidden;
  margin-top: 0.35rem;
}

.app-meter-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--mv-primary);
  /* Width is set inline from a server-computed percentage. */
  transition: width 400ms ease;
}

.app-meter-fill.is-success { background: var(--mv-success); }
.app-meter-fill.is-warning { background: var(--mv-warning); }
.app-meter-fill.is-primary { background: var(--mv-primary); }

/* --- Progress dial ------------------------------------------------------
   conic-gradient off a single custom property, so the percentage is set
   once inline and nothing here needs a charting library. */
.app-dial {
  --app-dial-value: 0;
  width: 4.25rem;
  height: 4.25rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background:
    conic-gradient(var(--mv-primary) calc(var(--app-dial-value) * 1%), var(--mv-surface-muted) 0);
  margin: 0.2rem 0;
}

.app-dial > span {
  width: 3.15rem;
  height: 3.15rem;
  border-radius: 50%;
  background: var(--mv-surface);
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: 1.05rem;
  color: var(--mv-text);
}

.app-dial small {
  font-size: 0.65rem;
  font-weight: 700;
  color: var(--mv-muted);
}

/* --- Package cards ------------------------------------------------------ */
.app-package-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.9rem;
}

.app-package {
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius);
  box-shadow: var(--mv-shadow-xs);
  padding: 1rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}

.app-package.is-active {
  border-color: var(--mv-primary);
  box-shadow: var(--mv-shadow-sm);
}

.app-package > header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.app-package h3 {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 700;
}

.app-package-level {
  font-weight: 800;
  color: var(--mv-primary-strong);
  background: var(--mv-primary-soft);
  border-radius: var(--mv-radius-sm);
  padding: 0.1rem 0.5rem;
  font-size: 0.85rem;
}

.app-package-price {
  margin: 0;
  font-size: 1.35rem;
  font-weight: 800;
}

.app-package-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.35rem 0.75rem;
  margin: 0;
}

.app-package-facts dt {
  font-size: 0.75rem;
  color: var(--mv-muted);
  font-weight: 600;
}

.app-package-facts dd {
  margin: 0;
  font-weight: 700;
  font-size: 0.92rem;
}

.app-package-origin {
  margin: 0;
  font-size: 0.78rem;
  color: var(--mv-muted);
}

/* --- Written record timeline -------------------------------------------- */
.app-timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
}

.app-timeline > li {
  background: var(--mv-surface-muted);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  border-inline-start: 3px solid var(--mv-primary);
  padding: 0.65rem 0.9rem;
}

.app-timeline-when {
  font-size: 0.78rem;
  color: var(--mv-muted);
  margin-inline-end: 0.5rem;
}

.app-timeline-source {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--mv-primary-strong);
  background: var(--mv-primary-soft);
  border-radius: 999px;
  padding: 0.1rem 0.55rem;
}

.app-timeline > li > p {
  margin: 0.35rem 0 0;
  color: var(--mv-text);
}

/* A count on a tab: how many rows are behind it, before opening it. */
.app-tab-count {
  display: inline-block;
  min-width: 1.35rem;
  margin-inline-start: 0.35rem;
  padding: 0 0.35rem;
  border-radius: 999px;
  background: var(--mv-surface-muted);
  color: var(--mv-muted);
  font-size: 0.75rem;
  font-weight: 700;
  text-align: center;
}

.nav-link.active .app-tab-count {
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
}

/* --- Student register: a visual list, not a spreadsheet -----------------
   Same rows the table had, each carrying the answer to "and how are they
   doing?" so the admin does not have to open every profile to find out. */
.app-state-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 1rem;
}

.app-state-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.35rem 0.8rem;
  border-radius: 999px;
  border: 1px solid var(--mv-border);
  background: var(--mv-surface);
  color: var(--mv-muted);
  font-size: 0.85rem;
  font-weight: 700;
  text-decoration: none;
  transition: border-color 150ms ease, color 150ms ease, background-color 150ms ease;
}

.app-state-chip > span {
  min-width: 1.4rem;
  padding: 0 0.3rem;
  border-radius: 999px;
  background: var(--mv-surface-muted);
  color: var(--mv-text);
  font-size: 0.78rem;
  text-align: center;
}

.app-state-chip:hover,
.app-state-chip:focus-visible {
  color: var(--mv-text);
  border-color: var(--mv-muted);
}

/* The chip carries its state's own colour on the left edge only - six fully
   coloured pills would compete with the badges on the cards below. */
.app-state-chip.is-active { border-inline-start: 3px solid var(--mv-success); }
.app-state-chip.is-startingsoon { border-inline-start: 3px solid var(--mv-primary); }
.app-state-chip.is-paymentdue { border-inline-start: 3px solid var(--mv-danger); }
.app-state-chip.is-endingsoon { border-inline-start: 3px solid var(--mv-warning); }
.app-state-chip.is-completed { border-inline-start: 3px solid var(--mv-accent); }
.app-state-chip.is-inactive { border-inline-start: 3px solid var(--mv-muted); }

.app-state-chip.is-on {
  background: var(--mv-primary-soft);
  border-color: var(--mv-primary);
  color: var(--mv-primary-strong);
}

.app-student-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: 0.85rem;
}

.app-student-card {
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius);
  box-shadow: var(--mv-shadow-xs);
  padding: 0.95rem 1.05rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-width: 0;
}

.app-student-card.needs-attention {
  border-inline-start: 3px solid var(--mv-warning);
}

.app-student-card > header {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: 0.5rem;
}

.app-student-name {
  font-weight: 800;
  font-size: 1.05rem;
  color: var(--mv-text);
  text-decoration: none;
}

.app-student-name:hover,
.app-student-name:focus-visible {
  color: var(--mv-primary-strong);
  text-decoration: underline;
}

.app-student-line {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0;
}

.app-student-metric {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.app-student-metric-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.85rem;
  color: var(--mv-muted);
}

.app-student-metric-head > strong {
  color: var(--mv-text);
  font-weight: 700;
}

.app-student-metric-note {
  font-size: 0.8rem;
}

.app-student-metric-note.is-bad {
  color: var(--mv-danger);
  font-weight: 700;
}

.app-student-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: 0.3rem 0.75rem;
  margin: 0;
}

.app-student-facts dt {
  font-size: 0.72rem;
  color: var(--mv-muted);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.app-student-facts dd {
  margin: 0;
  font-size: 0.88rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.app-student-card > footer {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: auto;
  padding-top: 0.35rem;
}

/* The search box's empty state and the "no students at all" state both live
   inside the grid, so they must span it rather than sit in one column. */
.app-student-grid > .app-empty {
  grid-column: 1 / -1;
}

/* --- Numbered flow inside a guide box ----------------------------------- */
.app-flow {
  list-style: none;
  counter-reset: mv-flow;
  margin: 0 0 0.75rem;
  padding: 0;
  display: grid;
  gap: 0.55rem;
}

.app-flow > li {
  counter-increment: mv-flow;
  display: grid;
  gap: 0.15rem;
  padding-inline-start: 2.1rem;
  position: relative;
}

.app-flow > li::before {
  content: counter(mv-flow);
  position: absolute;
  inset-inline-start: 0;
  top: 0.05rem;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
  font-size: 0.8rem;
  font-weight: 800;
  display: grid;
  place-items: center;
}

.app-flow > li > span {
  color: var(--mv-muted);
  font-size: 0.9rem;
}

/* --- Package posters ----------------------------------------------------
   A tall card per package rather than a row in a price list. The "face" is
   a CSS gradient carrying the level code - deliberately not an uploaded
   image: adding a real picture per package needs a new column and a file
   store, which is a decision for the owner, not something to slip in here.

   The rail scrolls sideways on narrow screens with the cards snapping into
   place, so the coverflow feel costs no library and no JavaScript. */
.app-poster-rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(16rem, 20rem);
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-bottom: 0.75rem;
  margin-bottom: 1.5rem;
  /* The rail is the only thing that scrolls sideways; the page never does. */
  overscroll-behavior-x: contain;
}

@media (min-width: 62rem) {
  /* With room, stop being a rail and just be a grid - nobody should have to
     scroll to discover a package that would have fit on screen. */
  .app-poster-rail {
    grid-auto-flow: row;
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    grid-auto-columns: auto;
    overflow-x: visible;
  }
}

.app-poster {
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-lg);
  box-shadow: var(--mv-shadow-sm);
  overflow: hidden;
  transition: transform 200ms ease, box-shadow 200ms ease;
}

.app-poster:hover,
.app-poster:focus-within {
  transform: translateY(-3px);
  box-shadow: var(--mv-shadow);
}

.app-poster-face {
  position: relative;
  min-height: 8.5rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  gap: 0.2rem;
  padding: 1rem 1.15rem;
  color: #fff;
  background:
    radial-gradient(14rem 9rem at 85% 10%, rgba(255, 255, 255, 0.22), transparent 65%),
    linear-gradient(135deg, var(--mv-primary-strong), var(--mv-accent));
}

.app-poster-level {
  font-size: 2.4rem;
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0.02em;
}

.app-poster-kind {
  font-size: 0.85rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  opacity: 0.9;
}

.app-poster-body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 1rem 1.15rem 1.15rem;
  flex: 1 1 auto;
}

.app-poster-body h3 {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 700;
}

.app-poster-price {
  margin: 0;
}

.app-poster-price > strong {
  font-size: 1.75rem;
  font-weight: 800;
  color: var(--mv-primary-strong);
}

.app-poster-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.4rem 0.75rem;
  margin: 0;
  padding: 0.7rem 0;
  border-top: 1px solid var(--mv-border);
  border-bottom: 1px solid var(--mv-border);
}

.app-poster-facts dt {
  font-size: 0.72rem;
  color: var(--mv-muted);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.app-poster-facts dd {
  margin: 0;
  font-weight: 700;
}

.app-poster-note {
  margin: 0;
  font-size: 0.8rem;
  color: var(--mv-muted);
}

/* The lift is decoration; anyone who asked for less motion keeps the card
   still. (The global reduced-motion block already zeroes the duration; this
   removes the displacement itself.) */
@media (prefers-reduced-motion: reduce) {
  .app-poster:hover,
  .app-poster:focus-within {
    transform: none;
  }
}

/* --- A message the admin sends by hand ----------------------------------
   Styled as a draft, not as a send button, so nobody reads it as "the
   system will notify them". Nothing here sends anything. */
.app-draft {
  background: var(--mv-surface-muted);
  border: 1px dashed var(--mv-border);
  border-radius: var(--mv-radius-sm);
  padding: 0.85rem 1rem;
  margin-bottom: 1rem;
  display: grid;
  gap: 0.5rem;
}

.app-draft-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.app-draft textarea {
  background: var(--mv-surface);
  font-size: 0.92rem;
  resize: vertical;
}

.app-draft-note {
  margin: 0;
  font-size: 0.8rem;
  color: var(--mv-muted);
}

/* ==========================================================================
   2026-09-01 — the register back as a table, the student's own standing,
   offer posters, and the split-screen sign-in. All additive: nothing above
   this line was changed, so every screen that did not ask for one of these
   looks exactly as it did.
   ========================================================================== */

/* --- Register table ------------------------------------------------------ */
.app-register {
  --mv-register-line: color-mix(in srgb, var(--mv-border) 75%, transparent);
  border-collapse: separate;
  border-spacing: 0;
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius);
  overflow: hidden;
  box-shadow: var(--mv-shadow-xs);
}

.app-register > thead > tr > th {
  background: var(--mv-surface-muted);
  border-bottom: 1px solid var(--mv-border);
  color: var(--mv-muted);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  padding: 0.7rem 0.75rem;
}

.app-register > tbody > tr > td,
.app-register > tbody > tr > th {
  border-top: 1px solid var(--mv-register-line);
  padding: 0.7rem 0.75rem;
  vertical-align: top;
  font-weight: 400;
  background: transparent;
}

.app-register > tbody > tr:first-child > td,
.app-register > tbody > tr:first-child > th {
  border-top: 0;
}

.app-register > tbody > tr:hover > td,
.app-register > tbody > tr:hover > th {
  background: var(--mv-primary-soft);
}

.app-register > tbody > tr.is-attention > th {
  box-shadow: inset 3px 0 0 var(--mv-warning);
}

[dir="rtl"] .app-register > tbody > tr.is-attention > th {
  box-shadow: inset -3px 0 0 var(--mv-warning);
}

/* A record's name: the primary way into its file, but not a raw blue link. */
.app-record-link {
  display: block;
  color: inherit;
  text-decoration: none;
  border-radius: var(--mv-radius-sm);
  padding: 0.1rem 0.15rem;
  margin: -0.1rem -0.15rem;
}

.app-record-link:hover .app-record-name,
.app-record-link:focus-visible .app-record-name {
  color: var(--mv-primary-strong);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.app-record-link:focus-visible {
  outline: 2px solid var(--mv-focus);
  outline-offset: 2px;
}

.app-record-name {
  display: block;
  font-weight: 700;
  color: var(--mv-text);
  line-height: 1.3;
}

.app-record-sub {
  display: block;
  font-size: 0.75rem;
  color: var(--mv-muted);
}

.app-cell-figure {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.app-cell-figure.is-bad { color: var(--mv-danger); }

.app-meter-sm {
  height: 0.3rem;
  margin-top: 0.3rem;
  max-width: 8rem;
}

.app-row-actions {
  display: flex;
  flex-wrap: wrap;
  /* ⭐ 2026-09-19: 0.3rem put two live buttons 4.8px apart, which on a phone is
     close enough to mis-tap Revoke instead of Edit. 0.5rem is the 8px
     adjacent-target minimum and costs one row of table width at most. The
     tighter Bootstrap gap-1 pairs elsewhere are deliberately left alone —
     those are an input beside its own submit, not two rival tap targets. */
  gap: 0.5rem;
  justify-content: flex-end;
}

/* --- A one-line "here is the situation, here is the button" band --------- */
.app-callout {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.95rem 1.1rem;
  margin-bottom: 1.5rem;
  border: 1px solid var(--mv-border);
  border-inline-start: 4px solid var(--mv-muted);
  border-radius: var(--mv-radius);
  background: var(--mv-surface);
  box-shadow: var(--mv-shadow-xs);
}

.app-callout > div { min-width: min(100%, 22rem); }
.app-callout p { color: var(--mv-muted); }

.app-callout.is-warning {
  border-inline-start-color: var(--mv-warning);
  background: var(--mv-warning-soft);
}

.app-callout.is-info {
  border-inline-start-color: var(--mv-primary);
  background: var(--mv-primary-soft);
}

.app-stat.is-good { border-inline-start: 3px solid var(--mv-success); }
.app-stat.is-bad { border-inline-start: 3px solid var(--mv-danger); }

/* --- Student file: whose file am I in? ----------------------------------- */
.app-profile-kicker {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  margin-bottom: 0.3rem;
  border-radius: 999px;
  background: var(--mv-accent-soft);
  color: var(--mv-accent);
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.app-profile-name {
  color: var(--mv-primary-strong);
  font-weight: 800;
}

/* --- Offer posters ------------------------------------------------------- */
.app-offer-rail {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1rem;
  margin-bottom: 2rem;
}

.app-poster-image {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--mv-surface-muted);
  display: flex;
  align-items: center;
  justify-content: center;
}

.app-poster-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.app-poster-placeholder {
  color: var(--mv-muted);
  font-size: 0.85rem;
}

.app-poster-thumb {
  width: 3rem;
  height: 4rem;
  object-fit: cover;
  border-radius: var(--mv-radius-sm);
  border: 1px solid var(--mv-border);
}

.app-poster-offer .app-poster-body h3 {
  font-size: 1.05rem;
  margin-bottom: 0.35rem;
}

/* --- Split-screen sign-in ------------------------------------------------ */
/* ⭐ Owner request 2026-09-15 (second pass): the photo is now genuinely
   full-screen — same breakout technique as .home-hero (negate
   .app-main.public's own --public-pad instead of the 100vw trick, which
   counted the scrollbar and broke the sticky topbar; see .home-hero's own
   comment for the full story) — and the sign-in card floats on top of it
   instead of sitting in a separate column beside it. isolation:isolate
   plus position:relative + z-index on .auth-card-wrap below guarantees the
   card paints above the absolutely-positioned photo/scrim regardless of
   DOM order (position:static elements would otherwise paint BEHIND any
   positioned sibling, photo included). */
.auth-hero {
  position: relative;
  margin-inline: calc(-1 * var(--public-pad, 0px));
  margin-block-start: calc(-1 * var(--public-pad, 0px)); /* see .home-hero */
  margin-block-end: calc(-1 * var(--public-pad, 0px));
  min-height: calc(100vh - var(--mv-topbar));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding-inline: clamp(1.5rem, 5vw, 5rem);
  overflow: hidden;
  isolation: isolate;
}

/* Side-by-side layout: marketing copy on one side, login card on the other.
   On small screens (< 992px) they stack vertically automatically. */
.auth-content-split {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: clamp(1.5rem, 4vw, 4rem);
  z-index: 2;
  width: 100%;
  max-width: 100%;
  flex-wrap: wrap;
}

@media (max-width: 991px) {
  .auth-content-split {
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
}

/* No-photo fallback keeps the original coloured panel — full-screen now
   too, same gradient, same drifting orbs. */
.auth-hero:not(.has-photo) {
  background:
    radial-gradient(120% 90% at 15% 10%, rgba(124, 58, 237, 0.85), transparent 60%),
    linear-gradient(140deg, var(--mv-primary-strong), var(--mv-primary) 55%, var(--mv-accent));
}

.auth-hero-photo {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  /* One screen tall at most: a long form (registration, or any page showing
     validation errors) makes the hero taller than the viewport, and a photo
     contained in that whole height floated down with blurred bands above it.
     Pinned to the first screen instead; the blurred backdrop continues below. */
  block-size: min(100%, calc(100vh - var(--mv-topbar)));
  object-fit: fill;
  object-position: center;
}

/* Darkens the inline-start side (where .auth-hero-copy's white/gold text
   sits) for contrast, fading to nothing by mid-photo — the card on the
   inline-end side is opaque and needs no legibility help from this. */
/* ⭐ Owner request 2026-09-17: full-screen dark transparent overlay for
   a professional cinematic feel — uniform rgba instead of the one-sided
   gradient, so every part of the photo is equally dimmed. */
.auth-hero-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.52);
  /* Subtle vignette on edges for depth */
  box-shadow: inset 0 0 120px rgba(0, 0, 0, 0.35);
}

/* The motion: three soft shapes drifting behind the copy. Transform and
   opacity only, so it stays on the compositor and never reflows the page. */
.auth-orb {
  position: absolute;
  border-radius: 999px;
  filter: blur(2px);
  opacity: 0.45;
  will-change: transform;
}

.auth-orb-1 {
  width: 22rem;
  height: 22rem;
  inset-block-start: -6rem;
  inset-inline-end: -5rem;
  background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0));
  animation: auth-drift-a 18s ease-in-out infinite;
}

.auth-orb-2 {
  width: 16rem;
  height: 16rem;
  inset-block-end: -4rem;
  inset-inline-start: -3rem;
  background: radial-gradient(circle at 60% 40%, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0));
  animation: auth-drift-b 22s ease-in-out infinite;
}

.auth-orb-3 {
  width: 11rem;
  height: 11rem;
  inset-block-start: 38%;
  inset-inline-end: 22%;
  background: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0));
  animation: auth-drift-c 26s ease-in-out infinite;
}

@keyframes auth-drift-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(-3rem, 2.5rem, 0) scale(1.12); }
}

@keyframes auth-drift-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(2.5rem, -2rem, 0) scale(0.9); }
}

@keyframes auth-drift-c {
  0%, 100% { transform: translate3d(0, 0, 0); }
  33% { transform: translate3d(-1.5rem, -1.5rem, 0); }
  66% { transform: translate3d(1.75rem, 1rem, 0); }
}

/* ⭐ Owner request 2026-09-15 (third pass, then a follow-up): back on the
   photo, on the inline-start side — hard-coded white/gold rather than
   --mv-* tokens, because this sits directly on a photo in BOTH light and
   dark mode (unlike the sign-in card, which really does need to flip
   surface colour with the theme). The page-wide .auth-hero-scrim helps but
   is direction-dependent (built for two side-by-side columns) and is not
   enough on its own wherever the photo underneath is bright — confirmed
   live on mobile, where it stacks full-width above the card (see the
   breakpoint below) and text crossed a sunlit window almost illegibly. This
   panel is the real, robust fix: its own frosted backdrop guarantees
   contrast regardless of what is behind it in the photo. */
/* Owner feedback 2026-09-16: the first version of this panel read as a
   solid dark box sitting on the photo rather than glass — turned down the
   tint a lot (0.4 → 0.2) and tightened the padding so more of the photo
   actually shows through it and around it; the blur is what keeps text
   legible now, not the tint. */
.auth-hero-copy {
  position: relative;
  z-index: 1;
  flex: 1 1 20rem;
  max-width: 32rem;
  padding: 1.5rem 1.75rem;
  border-radius: var(--mv-radius-lg);
  background: rgba(6, 10, 20, 0.25);
  backdrop-filter: blur(20px) saturate(1.5);
  -webkit-backdrop-filter: blur(20px) saturate(1.5);
  animation: auth-rise 700ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

.auth-hero-copy h2 {
  color: #ffd873;
  font-size: clamp(1.7rem, 3.2vw, 2.6rem);
  font-weight: 800;
  line-height: 1.15;
  margin-bottom: 0.65rem;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.4);
}

.auth-hero-copy-sub {
  max-width: 28rem;
  margin: 0 0 1.1rem;
  color: rgba(255, 255, 255, 0.92);
  font-size: 1rem;
  line-height: 1.5;
}

.auth-hero-copy-points {
  list-style: none;
  padding: 0;
  margin: 0 0 1.1rem;
  display: grid;
  gap: 0.6rem;
}

.auth-hero-copy-points li {
  position: relative;
  padding-inline-start: 1.85rem;
  color: rgba(255, 255, 255, 0.95);
  font-size: 0.92rem;
  font-weight: 600;
}

/* A real checkmark badge instead of a plain dot — the "✓" list the owner
   asked for. */
.auth-hero-copy-points li::before {
  content: "\2713";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.05rem;
  display: inline-grid;
  place-items: center;
  width: 1.2rem;
  height: 1.2rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.22);
  color: #fff;
  font-size: 0.68rem;
  font-weight: 800;
}

.auth-hero-copy-summary {
  margin: 0 0 0.5rem;
  color: rgba(255, 255, 255, 0.92);
  font-size: 0.92rem;
  font-style: italic;
}

.auth-hero-copy-cta {
  margin: 0;
  color: #ffd873;
  font-size: 1rem;
  font-weight: 800;
}

/* Floats the card over the photo instead of a separate column beside it —
   position:relative + z-index:1 is what keeps it painting above the
   absolutely-positioned photo/scrim (see .auth-hero's own comment). Pushed
   to the inline-end side (flex-end on .auth-hero) with room around it so
   the photo stays the dominant visual, not centred over it. */
.auth-card-wrap {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  width: 100%;
  max-width: 26rem;
  margin-block: clamp(2rem, 6vh, 4rem);
}

.auth-card-wrap > .auth-card {
  width: 100%;
  padding: clamp(1.75rem, 3.2vw, 3rem);
  animation: auth-rise 600ms cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: 80ms;
}

.auth-card-head { margin-bottom: 1.25rem; }

.auth-card-head h1 {
  color: var(--mv-primary-strong);
  font-size: clamp(1.35rem, 2.4vw, 1.8rem);
  font-weight: 800;
  line-height: 1.2;
}

.auth-card-head p {
  color: var(--mv-muted);
  font-size: 0.92rem;
  margin-bottom: 0;
}

.auth-card-foot {
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--mv-border);
}

/* ⭐ 2026-09-17: the only surviving route to /Privacy for a signed-out
   visitor now that the shared footer is gone (see .app-footer's removal) —
   deliberately quiet: small, muted, and never competing with the sign-in or
   registration action above it. */
.auth-card-privacy {
  margin-top: 0.6rem;
  text-align: center;
  font-size: 0.78rem;
}

/* The link colour, not the muted one: --mv-muted on the near-white card
   measures 4.2:1 at this size, under the 4.5:1 small-text minimum. The brand
   link colour is 5.85:1 in light mode and 6.14:1 in dark, and it stays quiet
   because the type is small and centred. */
.auth-card-privacy a {
  color: var(--mv-primary);
}

.auth-card-privacy a:hover {
  color: var(--mv-accent);
}

/* --- Registration (same hero as sign-in) ------------------------------- */
/* ⭐ Owner request 2026-09-17: /Account/Register shares the sign-in page's
   full-screen photo hero and floating card; the rules below only add what a
   longer, two-step form needs. Tokens throughout, so light and dark mode and
   RTL follow automatically. */
.auth-card-wrap.is-wide {
  max-width: 31rem;
}

.auth-card-head .app-eyebrow {
  margin-block-end: 0.6rem;
}

.register-card h2 {
  margin: 0 0 0.3rem;
  color: var(--mv-primary-strong);
  font-size: 1.05rem;
  font-weight: 800;
  line-height: 1.3;
}

.register-card .register-option p,
.register-card .register-role p {
  margin: 0;
  color: var(--mv-muted);
  font-size: 0.9rem;
  line-height: 1.55;
}

.register-choice {
  display: grid;
  gap: 0.9rem;
}

.register-option {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.9rem 1rem;
  align-items: start;
  padding: 1.1rem 1.15rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-lg);
  background: var(--mv-surface-muted);
  transition: border-color 160ms ease, box-shadow 160ms ease;
}

.register-option:hover,
.register-option:focus-within {
  border-color: var(--mv-primary);
  box-shadow: var(--mv-shadow-sm);
}

.register-option > .btn {
  grid-column: 1 / -1;
}

.register-option-icon {
  display: inline-grid;
  place-items: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  border-radius: 999px;
  background: var(--mv-primary-soft);
  color: var(--mv-primary);
}

.register-option-icon svg {
  inline-size: 1.4rem;
  block-size: 1.4rem;
  stroke: currentColor;
  stroke-width: 1.7;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.register-role {
  display: flex;
  gap: 0.9rem;
  align-items: flex-start;
  margin-block-end: 1.25rem;
  padding-block-end: 1.1rem;
  border-block-end: 1px solid var(--mv-border);
}

.register-role .register-option-icon {
  flex: 0 0 auto;
}

.register-form .form-label {
  font-weight: 700;
}

.register-grid {
  display: grid;
  gap: 0 1rem;
}

@media (min-width: 30rem) {
  .register-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.register-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin-block-start: 0.5rem;
}

.register-actions .btn-primary {
  flex: 1 1 12rem;
}

@keyframes auth-rise {
  from { opacity: 0; transform: translate3d(0, 0.9rem, 0); }
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}

@media (max-width: 991.98px) {
  /* Stacked (copy above, card below — the same order as the markup) reads
     better narrow than two floating columns fighting for a photo's width. */
  .auth-hero {
    flex-direction: column;
    justify-content: center;
    min-height: auto;
    padding-block: 2.5rem;
    gap: 1.5rem;
  }

  /* The desktop scrim darkens left-to-right for two SIDE-BY-SIDE columns;
     stacked, the copy runs the full width above the card instead, so a
     horizontal fade left it washed out wherever it crossed a bright part of
     the photo (confirmed live — the checklist was barely legible over a
     sunlit window). Top-to-bottom instead, covering the copy's whole width. */
  .auth-hero-scrim {
    background: linear-gradient(180deg, rgba(6, 10, 20, 0.4) 0%, rgba(6, 10, 20, 0.78) 55%, rgba(6, 10, 20, 0.88) 100%);
  }

  /* ⭐ 2026-09-17: whole photo as a banner on top (see .home-hero's twin
     rule); copy and card follow on the blurred backdrop. */
  .auth-hero.has-photo {
    padding-block-start: 0;
  }

  .auth-hero.has-photo .auth-hero-photo {
    position: relative;
    order: -1;
    inline-size: calc(100% + 2 * clamp(1.5rem, 5vw, 5rem));
    max-inline-size: none;
    block-size: auto;
    max-block-size: 60vh;
    margin-inline: calc(-1 * clamp(1.5rem, 5vw, 5rem));
  }

  .auth-hero.has-photo .auth-hero-scrim {
    display: none;
  }

  /* On a phone the registration form is the page; the decorative panel
     (aria-hidden, and a repeat of the sign-in page's copy) would push it
     a full screen further down, so it is left out here. */
  .auth-hero-register .auth-hero-copy {
    display: none;
  }

  .auth-hero-copy {
    max-width: 30rem;
  }

  .auth-card-wrap {
    max-width: 26rem;
    margin-block: 0;
  }
}

/* Decoration only — it all stops for anyone who asked it to. */
@media (prefers-reduced-motion: reduce) {
  .auth-orb,
  .auth-hero-copy,
  .auth-card-wrap > .auth-card {
    animation: none !important;
  }
}

/* The price / paid / remaining block that now sits above every confirm
   action and every package's payment history — the figures an admin needs
   in view before typing an amount. */
.app-owed-breakdown {
  background: var(--mv-surface-muted);
  border-radius: var(--mv-radius-sm);
  padding: 0.6rem 0.75rem;
  margin-bottom: 0.75rem;
}

.app-owed-breakdown dt {
  color: var(--mv-muted);
  font-weight: 600;
}

.app-owed-breakdown dd {
  margin-bottom: 0.15rem;
  font-variant-numeric: tabular-nums;
}

/* /Admin/Students UX pass (2026-09-03) — plain Bootstrap accordion for the
   "Single corrections" forms. The vendored bootstrap.min.css is the LTR
   build only (the app flips direction via [dir="rtl"] on <html>, not via a
   separate RTL stylesheet — see every other [dir="rtl"] rule in this file),
   so the accordion header's own left-aligned text and left-pushed chevron
   need the same manual mirroring already used elsewhere in this file. */
[dir="rtl"] .accordion-button {
  text-align: right;
}

[dir="rtl"] .accordion-button::after {
  margin-left: 0;
  margin-right: auto;
}

/* ⭐ 2026-09-11 — public placement onboarding (verify phone, profile),
   the Placement page and the join window. Built only from the existing
   tokens above, so nothing here introduces a new colour or radius. */
.app-onboarding {
  max-width: 40rem;
  margin-inline: auto;
}

/* Its own class, NOT .app-steps: .app-steps is the admin family-registration
   stepper (above), and sharing the name made these circle-sized spans clip
   its step labels (2026-09-11 fix). */
.app-onboarding-steps {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  list-style: none;
  padding: 0;
  margin: 0 0 1.25rem;
}

.app-onboarding-steps li {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--mv-muted);
  font-size: 0.875rem;
  font-weight: 600;
}

.app-onboarding-steps li + li::before {
  content: "";
  width: 1.25rem;
  height: 2px;
  background: var(--mv-border);
}

.app-onboarding-steps span {
  display: inline-grid;
  place-items: center;
  width: 1.65rem;
  height: 1.65rem;
  border-radius: 999px;
  background: var(--mv-surface-muted);
  color: var(--mv-muted);
  font-size: 0.8rem;
}

.app-onboarding-steps .is-done span {
  background: var(--mv-success-soft);
  color: var(--mv-success);
}

.app-onboarding-steps .is-current {
  color: var(--mv-text);
}

.app-onboarding-steps .is-current span {
  background: var(--mv-primary);
  color: #fff;
}

/* The package a visitor picked on the public catalogue. */
.app-poster.is-selected {
  outline: 3px solid var(--mv-primary);
  outline-offset: 2px;
}

/* --- Public storefront: /Packages ----------------------------------------
   The shop window, and the page the owner is treating as the reference for
   a later whole-site redesign - so it stays on the existing --mv-* tokens
   and adds no library. Logical properties throughout, so Arabic RTL is a
   designed mirror rather than a patched one; the only [dir="rtl"] rules
   below are the two that genuinely cannot be expressed logically (an SVG
   arrow's direction, and a decorative gradient's angle).

   Composition after three 21st.dev references studied as whole pages:
   ln-dev7/pricing-16 (cards wide enough to fill the measure, hairline rules
   between header / price / specs / CTA, badge on the card's top edge),
   tommyjepsen/hero-with-image-text-and-two-buttons (split hero proportion
   and a defined image panel) and mikolajdobrucki/pricing (section rhythm,
   tinted wash on the highlighted card). Colour lives in the image panel,
   not in a full-bleed gradient over the whole hero. */
.mv-market {
  --mv-market-gap: clamp(2.5rem, 5vw, 4.5rem);
  max-width: 82rem;
  margin-inline: auto;
}

/* ---- Hero -------------------------------------------------------------- */
/* Owner request 2026-09-17: the illustration panel and the explanatory lead
   paragraph were removed. The hero is now one band - title and actions on the
   reading-start side, the counted facts beside them on wide screens (under
   them when narrow) - closed by a hairline, so it reads as a deliberate page
   header rather than half of a two-column layout with a column missing. */
.mv-hero {
  margin-block-end: var(--mv-market-gap);
  padding-block: clamp(1rem, 3vw, 2.5rem) clamp(1.5rem, 3vw, 2.5rem);
  border-block-end: 1px solid var(--mv-border);
}

.mv-hero-copy {
  display: grid;
  gap: 1.15rem;
  justify-items: start;
}

@media (min-width: 60rem) {
  .mv-hero-copy {
    grid-template-columns: minmax(0, 1fr) minmax(17rem, 24rem);
    column-gap: clamp(2rem, 5vw, 4.5rem);
    align-items: end;
  }

  .mv-hero-copy > * {
    grid-column: 1;
  }

  .mv-hero-copy > .mv-hero-facts {
    grid-column: 2;
    grid-row: 1 / span 3;
    align-self: end;
    margin: 0;
    padding-block-start: 0;
    padding-inline-start: clamp(1.25rem, 3vw, 2.25rem);
    border-block-start: 0;
    border-inline-start: 1px solid var(--mv-border);
  }
}

.mv-hero-copy h1 {
  margin: 0;
  font-size: clamp(2.1rem, 4.2vw, 3.4rem);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.015em;
  color: var(--mv-primary-strong);
  text-wrap: balance;
}

.mv-hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-block-start: 0.35rem;
}

/* Counted from the packages on this page - never an invented claim. */
.mv-hero-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0.75rem 0 0;
  padding-block-start: 1.25rem;
  border-block-start: 1px solid var(--mv-border);
  inline-size: 100%;
}

.mv-hero-facts > div {
  flex: 1 1 6rem;
  min-inline-size: 0;
}

.mv-hero-facts dt {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--mv-muted);
}

.mv-hero-facts dd {
  margin: 0.15rem 0 0;
  font-size: 1.6rem;
  font-weight: 800;
  line-height: 1;
  color: var(--mv-primary);
}

/* ---- Buttons ----------------------------------------------------------- */
/* Own button, rather than Bootstrap's: the sign-in control here had to be
   readable on sight in every state, which the outline variant on a coloured
   ground was not. Both variants below sit on white and are checked for
   contrast: primary is #fff on --mv-primary, secondary is
   --mv-primary-strong on #fff with a 2px brand border. */
.mv-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.72rem 1.4rem;
  border: 2px solid transparent;
  border-radius: 999px;
  font-size: 0.98rem;
  font-weight: 700;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 180ms ease, border-color 180ms ease,
    color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
}

.mv-btn.is-block {
  inline-size: 100%;
}

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

.mv-btn.is-primary {
  background: var(--mv-primary);
  border-color: var(--mv-primary);
  color: #fff;
  box-shadow: 0 10px 24px -12px rgba(37, 84, 199, 0.9);
}

.mv-btn.is-primary:hover,
.mv-btn.is-primary:focus {
  background: var(--mv-primary-strong);
  border-color: var(--mv-primary-strong);
  color: #fff;
  transform: translateY(-1px);
}

.mv-btn.is-primary:active {
  transform: translateY(0);
}

.mv-btn.is-secondary {
  background: var(--mv-surface);
  border-color: var(--mv-primary);
  color: var(--mv-primary-strong);
}

.mv-btn.is-secondary:hover,
.mv-btn.is-secondary:focus {
  background: var(--mv-primary-soft);
  border-color: var(--mv-primary-strong);
  color: var(--mv-primary-strong);
}

.mv-btn-arrow {
  inline-size: 1.05rem;
  block-size: 1.05rem;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 180ms ease;
}

/* The arrow points the way the language reads. */
[dir="rtl"] .mv-btn-arrow {
  transform: scaleX(-1);
}

.mv-btn:hover .mv-btn-arrow {
  transform: translateX(0.2rem);
}

[dir="rtl"] .mv-btn:hover .mv-btn-arrow {
  transform: scaleX(-1) translateX(0.2rem);
}

/* ---- A course, and the packages it publishes --------------------------- */
/* The panel is what makes plans of the same course read as one offer
   instead of loose cards on a page. */
.mv-course {
  padding: clamp(1.25rem, 2.6vw, 2.1rem);
  margin-block-end: clamp(1.5rem, 3vw, 2.25rem);
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-xl);
  box-shadow: var(--mv-shadow-sm);
}

.mv-course-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  margin-block-end: clamp(1rem, 2vw, 1.5rem);
  padding-block-end: 0.9rem;
  border-block-end: 1px solid var(--mv-border);
}

.mv-course-head h2 {
  margin: 0;
  font-size: clamp(1.25rem, 2vw, 1.6rem);
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--mv-primary-strong);
}

.mv-course-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin: 0;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--mv-muted);
}

.mv-plan-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
  gap: clamp(0.9rem, 1.8vw, 1.4rem);
}

.mv-plan {
  position: relative;
  display: grid;
  gap: 1.1rem;
  padding: clamp(1.25rem, 2.2vw, 1.75rem);
  background: var(--mv-surface);
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-lg);
  transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
}

.mv-plan:hover,
.mv-plan:focus-within {
  transform: translateY(-4px);
  border-color: var(--mv-primary);
  box-shadow: var(--mv-shadow);
}

/* A course with a single package: two columns inside one card, so the row
   looks composed instead of one strip stretched across the panel. */
@media (min-width: 48rem) {
  .mv-plan-grid.is-solo .mv-plan {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
    align-items: center;
    gap: clamp(1.5rem, 3vw, 2.75rem);
  }
}

.mv-plan-lead {
  display: grid;
  gap: 0.85rem;
  align-content: start;
}

.mv-plan-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.mv-tag {
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
  background: var(--mv-surface-muted);
  color: var(--mv-muted);
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.mv-tag.is-level {
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
}

.mv-plan-price {
  display: grid;
  gap: 0.45rem;
  justify-items: start;
  margin: 0;
}

.mv-plan-price > strong {
  font-size: clamp(2.3rem, 3.6vw, 3rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--mv-primary-strong);
}

.mv-plan-unit {
  padding: 0.22rem 0.6rem;
  border-radius: 999px;
  background: var(--mv-surface-muted);
  color: var(--mv-muted);
  font-size: 0.79rem;
  font-weight: 600;
}

.mv-plan-cta {
  display: grid;
  gap: 0.5rem;
  justify-items: center;
  margin-block-start: 0.2rem;
}

.mv-plan-signin {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--mv-primary);
}

/* What is actually in the package: every published figure, one per row. */
.mv-plan-specs {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0.35rem 0.9rem;
  border-radius: var(--mv-radius);
  background: var(--mv-surface-muted);
}

.mv-plan-specs > div {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.62rem;
  border-block-end: 1px solid var(--mv-border);
}

.mv-plan-specs > div:last-child {
  border-block-end: 0;
}

.mv-plan-specs dt {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--mv-muted);
}

.mv-plan-specs dt::before {
  content: "";
  inline-size: 0.42rem;
  block-size: 0.42rem;
  flex: none;
  border-radius: 999px;
  background: var(--mv-primary);
}

.mv-plan-specs dd {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 800;
  color: var(--mv-text);
  text-align: end;
}

/* ---- The package this visitor chose ------------------------------------ */
/* Marked because they picked it - not "most popular", not "best value". */
.mv-plan.is-chosen {
  border-color: var(--mv-primary);
  border-width: 2px;
  background:
    linear-gradient(180deg, var(--mv-primary-soft), var(--mv-surface) 38%);
  box-shadow: 0 0 0 4px rgba(37, 84, 199, 0.1), var(--mv-shadow-sm);
}

.mv-plan-flag {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: clamp(1.25rem, 2.2vw, 1.75rem);
  transform: translateY(-50%);
  margin: 0;
  padding: 0.25rem 0.8rem;
  border-radius: 999px;
  background: var(--mv-primary);
  color: #fff;
  font-size: 0.73rem;
  font-weight: 700;
  box-shadow: var(--mv-shadow-xs);
}

.mv-market-note {
  margin: 0;
  padding-block-start: 0.5rem;
  font-size: 0.85rem;
  color: var(--mv-muted);
  text-align: center;
}

@media (max-width: 47.99rem) {
  .mv-hero-copy {
    justify-items: center;
    text-align: center;
  }

  .mv-hero-actions {
    justify-content: center;
    inline-size: 100%;
  }

  .mv-hero-actions .mv-btn {
    flex: 1 1 12rem;
  }

  .mv-course-head {
    flex-direction: column;
    align-items: flex-start;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mv-plan,
  .mv-btn,
  .mv-btn-arrow {
    transition: none;
  }

  .mv-plan:hover,
  .mv-plan:focus-within,
  .mv-btn.is-primary:hover,
  .mv-btn:hover .mv-btn-arrow {
    transform: none;
  }

  [dir="rtl"] .mv-btn:hover .mv-btn-arrow {
    transform: scaleX(-1);
  }
}

.app-onboarding-card {
  box-shadow: var(--mv-shadow-sm);
}

.app-code-input {
  max-width: 11rem;
  font-size: 1.35rem;
  letter-spacing: 0.35em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.app-gap-list {
  padding-inline-start: 1.1rem;
  margin-bottom: 1rem;
}

.app-gap-list li + li {
  margin-top: 0.3rem;
}

.app-placement-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
  gap: 1rem;
}

.app-placement-card {
  height: 100%;
}

.app-slot-list {
  display: grid;
  gap: 0.5rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

.app-slot-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-sm);
  background: var(--mv-surface);
}

.app-join-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.35rem 1rem;
  margin-bottom: 1rem;
}

.app-join-facts dt {
  color: var(--mv-muted);
  font-weight: 600;
}

.app-join-facts dd {
  margin: 0;
}

.app-join-status {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.75rem 0.9rem;
  border-radius: var(--mv-radius-sm);
  background: var(--mv-primary-soft);
  color: var(--mv-primary-strong);
}

.app-join-status[data-state="ready"] {
  background: var(--mv-success-soft);
  color: var(--mv-success);
}

.app-join-status[data-state="error"] {
  background: var(--mv-danger-soft);
  color: var(--mv-danger);
}

/* ==========================================================================
   Dark mode — additive only, everything below is scoped under
   [data-theme="dark"] on <html>. Nothing above this line is touched, so
   light mode (the default, no attribute present) renders exactly as before
   regardless of anything added here. Toggled by the button in the topbar
   (see _Layout.cshtml + js/site.js), persisted per-visitor in
   localStorage. Pure CSS + a few lines of vanilla JS, no new dependency,
   no markup, model, route, or business-logic change. ========================== */
:root[data-theme="dark"] {
  color-scheme: dark;

  /* Neutrals — the whole shell, every card/panel border, and all body text
     follow from these four. */
  --mv-bg: #0b1220;
  --mv-surface: #121a2c;
  --mv-surface-muted: #182338;
  --mv-text: #e8ecf6;
  --mv-muted: #98a4bc;
  --mv-border: #2a3550;

  /* Brand + status colours, brightened so they stay legible as TEXT sitting
     directly on the dark surfaces above (the same tokens are also used as a
     solid button/badge background with white text in a handful of places;
     those specific selectors are pinned back to their original light-mode
     hex further down so they keep their exact current look). */
  --mv-primary: #6d97f5;
  --mv-primary-strong: #9cbcff;
  --mv-primary-soft: #182644;
  --mv-accent: #b399fb;
  --mv-accent-soft: #251b3f;
  --mv-success: #4ade80;
  --mv-success-soft: #123322;
  --mv-warning: #fbbf24;
  --mv-warning-soft: #3a2a0e;
  --mv-danger: #f87171;
  --mv-danger-soft: #3a1917;
  --mv-focus: #6d97f5;

  /* ⭐ The navigation icon families, lifted for the dark surfaces. The light
     values are mid-tones chosen against white and read as mud on #121a2c, so
     each is replaced rather than reused — same eleven families, same
     meanings, same restraint, measured against --mv-surface instead. */
  --mv-tone-dashboard: #7ea6ff;
  --mv-tone-students: #4dd0e1;
  --mv-tone-teachers: #b399fb;
  --mv-tone-packages: #9aa5ff;
  --mv-tone-payments: #5cc98a;
  --mv-tone-promo: #f0b45e;
  --mv-tone-schedules: #4fc3b5;
  --mv-tone-compensation: #f59e6b;
  --mv-tone-reports: #5cc98a;
  --mv-tone-certificates: #d8b25a;
  --mv-tone-administration: #9aa8bf;

  --mv-empty-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2398a4bc' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 13h4l2 3h6l2-3h4'/%3E%3Cpath d='M5 13V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v7'/%3E%3Cpath d='M3 13v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4'/%3E%3C/svg%3E");
  --mv-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
  --mv-shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.45);
  --mv-shadow: 0 16px 40px -12px rgba(0, 0, 0, 0.6), 0 4px 14px -6px rgba(0, 0, 0, 0.4);
  --mv-shadow-lg: 0 28px 64px -16px rgba(0, 0, 0, 0.65);

  /* Bootstrap's OWN tokens, not ours — site.css only ever pointed
     --bs-body-color/--bs-body-bg at the --mv-* equivalents (see :root
     above), so every Bootstrap rule reading a DIFFERENT bs-* variable
     stayed on Bootstrap's light-mode default regardless of theme. Two real,
     user-reported bugs traced to exactly this:
     1. The placeholder text on every input ("Email or phone number" etc.)
        reads --bs-secondary-color, never repointed, so it rendered as
        Bootstrap's default near-black grey on the new dark input background.
     2. Every floating label (the same text) turns near-black too the moment
        the field is focused or filled — Bootstrap's own
        .form-floating > .form-control:focus ~ label rule (and the
        :not(:placeholder-shown) filled-state twin) reads
        rgba(var(--bs-body-color-rgb), .65), a SEPARATE token from
        --bs-body-color that was never repointed either, so it always used
        Bootstrap's own near-black default regardless of theme.
     Fixed at the token in both cases, so every input sitewide is covered,
     not just one field on one page. */
  --bs-body-color-rgb: 232, 236, 246;
  --bs-secondary-color: var(--mv-muted);
  --bs-secondary-color-rgb: 152, 164, 188;
  --bs-border-color-translucent: var(--mv-border);

  /* ⭐ 2026-09-17: with data-bs-theme="dark" now set too (see _Layout.cshtml),
     Bootstrap supplies dark values for everything above that site.css never
     mapped. These pull the ones that show as text or panels onto the same
     navy palette instead of Bootstrap's neutral greys, so a table cell, a
     disabled field or an input-group label matches the cards around it. */
  --bs-emphasis-color: var(--mv-text);
  --bs-emphasis-color-rgb: 232, 236, 246;
  --bs-heading-color: inherit;
  --bs-secondary-bg: var(--mv-surface-muted);
  --bs-tertiary-bg: var(--mv-surface-muted);
  --bs-link-color: var(--mv-primary);
  --bs-link-color-rgb: 109, 151, 245;
  --bs-link-hover-color: var(--mv-accent);
  --bs-link-hover-color-rgb: 179, 153, 251;
}


/* ⭐ 2026-09-19: what is left of the old restatement block. The rule it
   used to undo is gone (the panels above now read --mv-surface directly, so
   nothing flattens their states any more), and every restatement that was
   purely a token-backed background went with it. These two survive because
   their light rules set a BORDER colour as a literal hex that no token
   supplies, and a light border on a dark card is the one thing the token
   swap cannot fix by itself. */
:root[data-theme="dark"] .app-kpi.is-attention {
  border-color: #6b4f14;
  background: var(--mv-warning-soft);
}

:root[data-theme="dark"] .app-steps > li.is-done {
  border-color: #1f5a37;
  background: var(--mv-success-soft);
}

:root[data-theme="dark"] .app-guide {
  background: linear-gradient(180deg, var(--mv-primary-soft), var(--mv-surface) 65%);
}

:root[data-theme="dark"] .app-chart {
  background: linear-gradient(180deg, var(--mv-surface-muted), var(--mv-surface));
}

:root[data-theme="dark"] .app-topbar {
  background: rgba(18, 26, 44, 0.86);
}

:root[data-theme="dark"] .public-hero-panel > div {
  background: rgba(18, 26, 44, 0.86);
}

:root[data-theme="dark"] .auth-card {
  background: rgba(18, 26, 44, 0.94);
}

:root[data-theme="dark"] .app-theme-icon.is-sun {
  display: none;
}

:root[data-theme="dark"] .app-theme-icon.is-moon {
  display: block;
}

:root[data-theme="dark"] .alert-info {
  border-color: #2c5570;
  background: #102636;
  color: #bcdff5;
}

/* ⭐ 2026-09-17: success/warning/danger alerts already take their text and
   fill from the dark tokens, but their borders were light-mode hex values
   (#bfe6cd, #f3d49b, #f3c6c2) - bright outlines around dark boxes. */
:root[data-theme="dark"] .alert-success {
  border-color: #1f5a37;
}

:root[data-theme="dark"] .alert-warning {
  border-color: #6b4f14;
}

:root[data-theme="dark"] .alert-danger {
  border-color: #6e2a26;
}

:root[data-theme="dark"] .form-control,
:root[data-theme="dark"] .form-select,
:root[data-theme="dark"] textarea {
  background-color: var(--mv-surface);
  color: var(--mv-text);
}

/* ⭐ 2026-09-17: the rule above also beat Bootstrap's disabled styling, so a
   locked field looked exactly like one you could type in. Read-only fields
   are left alone on purpose: they hold text meant to be read and copied (a
   generated code, a message to send). */
:root[data-theme="dark"] .form-control:disabled,
:root[data-theme="dark"] .form-select:disabled,
:root[data-theme="dark"] textarea:disabled {
  background-color: var(--mv-surface-muted);
  color: var(--mv-muted);
}

:root[data-theme="dark"] .card,
:root[data-theme="dark"] .dropdown-menu,
:root[data-theme="dark"] .offcanvas,
:root[data-theme="dark"] .list-group,
:root[data-theme="dark"] .modal-content {
  --bs-card-bg: var(--mv-surface);
  --bs-dropdown-bg: var(--mv-surface);
  --bs-dropdown-link-color: var(--mv-text);
  --bs-dropdown-link-hover-bg: var(--mv-surface-muted);
  --bs-offcanvas-bg: var(--mv-surface);
  --bs-list-group-bg: var(--mv-surface);
  --bs-list-group-color: var(--mv-text);
  --bs-modal-bg: var(--mv-surface);
  background-color: var(--mv-surface);
  color: var(--mv-text);
}

/* ⭐ 2026-09-17: Bootstrap's outline buttons keep their stock mid-tone
   colours (#0d6efd, #dc3545, #198754, #ffc107, #6c757d) in either theme; on
   the dark surfaces those measured 3.7-4.2:1, under the 4.5:1 body-text
   minimum (Students, Schedules, Teachers, Placement tests, Reschedule). The
   text and border now use the brightened dark tokens; hovering fills with the
   same solid colours the light theme uses, carrying white text. */
:root[data-theme="dark"] .btn-outline-primary {
  --bs-btn-color: var(--mv-primary);
  --bs-btn-border-color: var(--mv-primary);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: #2554c7;
  --bs-btn-hover-border-color: #2554c7;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #16337e;
  --bs-btn-active-border-color: #16337e;
  --bs-btn-disabled-color: var(--mv-muted);
  --bs-btn-disabled-border-color: var(--mv-border);
}

:root[data-theme="dark"] .btn-outline-danger {
  --bs-btn-color: var(--mv-danger);
  --bs-btn-border-color: var(--mv-danger);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: #c0261e;
  --bs-btn-hover-border-color: #c0261e;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #9b1c16;
  --bs-btn-active-border-color: #9b1c16;
  --bs-btn-disabled-color: var(--mv-muted);
  --bs-btn-disabled-border-color: var(--mv-border);
}

:root[data-theme="dark"] .btn-outline-success {
  --bs-btn-color: var(--mv-success);
  --bs-btn-border-color: var(--mv-success);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: #15803d;
  --bs-btn-hover-border-color: #15803d;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #116932;
  --bs-btn-active-border-color: #116932;
  --bs-btn-disabled-color: var(--mv-muted);
  --bs-btn-disabled-border-color: var(--mv-border);
}

:root[data-theme="dark"] .btn-outline-warning {
  --bs-btn-color: var(--mv-warning);
  --bs-btn-border-color: var(--mv-warning);
  --bs-btn-hover-color: #1f1605;
  --bs-btn-hover-bg: #fbbf24;
  --bs-btn-hover-border-color: #fbbf24;
  --bs-btn-disabled-color: var(--mv-muted);
  --bs-btn-disabled-border-color: var(--mv-border);
}

:root[data-theme="dark"] .btn-outline-secondary {
  --bs-btn-disabled-color: var(--mv-muted);
  --bs-btn-disabled-border-color: var(--mv-border);
}

/* Solid-colour buttons/badges that carry white text: pinned to the exact
   light-mode hex so they render identically regardless of the brighter
   --mv-primary/--mv-success above (those exist only to keep plain TEXT
   readable on the new dark surfaces, not to repaint these). */
:root[data-theme="dark"] .app-brand-mark {
  background: linear-gradient(150deg, #2554c7, #16337e);
}

:root[data-theme="dark"] .btn-primary {
  background: linear-gradient(150deg, #2554c7, #16337e);
  border-color: #2554c7;
}

:root[data-theme="dark"] .btn-primary:hover {
  background: linear-gradient(150deg, #16337e, #16337e);
  border-color: #16337e;
}

:root[data-theme="dark"] .mv-btn.is-primary {
  background: #2554c7;
  border-color: #2554c7;
}

:root[data-theme="dark"] .mv-btn.is-primary:hover,
:root[data-theme="dark"] .mv-btn.is-primary:focus {
  background: #16337e;
  border-color: #16337e;
}

:root[data-theme="dark"] .app-step-badge,
:root[data-theme="dark"] .app-steps > li.is-current::before {
  background: #2554c7;
}

:root[data-theme="dark"] .app-steps > li.is-done::before,
:root[data-theme="dark"] .app-step-panel.is-done .app-step-badge {
  background: #15803d;
}

:root[data-theme="dark"] .app-chip.is-removable:hover,
:root[data-theme="dark"] .app-chip.is-removable:focus-visible,
:root[data-theme="dark"] .app-onboarding-steps .is-current span,
:root[data-theme="dark"] .mv-plan-flag {
  background: #2554c7;
}

/* Plain anchor text and the few shared headings/active-nav-state selectors
   that print --mv-primary-strong directly as foreground text — kept at the
   brightened token value above (nothing to pin here), listed only so the
   intent is on record: .app-page-heading h1, .home-hero-content h1 (no-photo
   fallback only — has-photo pins it to white, see above), .app-main >
   h1/h2, .app-nav-group.is-active, .app-nav-link.is-here/.active,
   .auth-card-head h1, .card-title all inherit the brighter
   --mv-primary-strong automatically and were checked live on Local Staging
   Published. */

@media (prefers-reduced-motion: reduce) {
  .app-join-status .spinner-border {
    animation-duration: 2.5s;
  }
}

/* --- Password recovery (⭐ launch 2026-09-25) ----------------------------
   The three /Account/ForgotPassword* pages reuse the sign-in hero and
   floating card; with no marketing copy beside the card it is centred.
   Tokens only, so light/dark mode and RTL follow automatically. */
.auth-hero-recovery {
  justify-content: center;
}

.auth-hero-recovery .auth-card-wrap {
  max-width: 28rem;
}

.auth-hero-recovery .app-onboarding-steps {
  margin-block-end: 1rem;
}

/* "Remember me" and "Forgot password?" share one row on the sign-in card;
   on a very narrow phone the link wraps under the checkbox instead of
   squeezing it. */
.auth-login-options {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
}

.auth-forgot-link {
  color: var(--mv-primary);
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
  /* A comfortable touch target without changing the row's look. */
  padding-block: 0.35rem;
}

.auth-forgot-link:hover,
.auth-forgot-link:focus-visible {
  color: var(--mv-accent);
  text-decoration: underline;
}

.auth-password-rules {
  padding: 0.75rem 1rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-lg);
  background: var(--mv-surface-muted);
  color: var(--mv-muted);
  font-size: 0.875rem;
}

.auth-password-rules ul {
  margin: 0;
  padding-inline-start: 1.1rem;
}

/* --- Package prices by country (⭐ 2026-09-25) ---------------------------
   The public catalogue's country picker: one labelled select (searchable
   through site.js's combobox) and a submit button, wrapping on a phone. */
.mv-country-picker {
  margin-block: 0 1.5rem;
  padding: 1rem 1.25rem;
  border: 1px solid var(--mv-border);
  border-radius: var(--mv-radius-lg);
  background: var(--mv-surface);
}

.mv-country-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: flex-start;
}

.mv-country-row > .app-combo,
.mv-country-row > select {
  flex: 1 1 16rem;
  min-width: 0;
}
