/* App-level styles — mobile-first, vanilla CSS (AGENTS.md §6) */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-family: var(--font-family);
  font-size: 16px;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Cross-document View Transitions: the browser snapshots the outgoing page and
   crossfades to the new one instead of flashing the default canvas. Progressive
   enhancement — Firefox ignores this block entirely (falls back to today's
   behavior). */
@view-transition { navigation: auto; }

/* Designed cross-document motion (2026-07-17): the outgoing page fades on the
   decelerate curve while the incoming one fades in with a gentle rise — one
   materialize gesture instead of the UA-default flat crossfade. Named funnel
   chrome (brand · progress bar · head) is snapshotted separately below, so it
   persists/morphs across steps instead of flashing with the root. */
@keyframes vt-fade-out {
  to { opacity: 0; }
}
@keyframes vt-rise-in {
  from { opacity: 0; transform: translateY(var(--space-2)); }
}
::view-transition-old(root) {
  animation: vt-fade-out var(--dur-moderate) var(--ease-decel);
}
::view-transition-new(root) {
  animation: vt-rise-in var(--dur-slower) var(--ease-decel);
}

html { scrollbar-gutter: stable; }

/* Accessible keyboard focus (M3 interaction states). :focus-visible only
   fires for keyboard/AT users, so mouse clicks keep the existing per-control
   focus styling with no added ring. Uses the M3 focus state colour. */
:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}
body.dark-page :focus-visible {
  outline-color: var(--orange);
}
/* Don't double-ring controls that already paint their own focus border. */
.pill-input:focus-visible,
.search-input:focus-visible,
.roster-table .pill-select:focus-visible { outline: none; }

body {
  min-height: 100dvh;
  line-height: 1.5;
}

/* --- Grain overlay (fixed, non-scrolling, pointer-events-none) --- */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: var(--z-grain);
  opacity: 0.03;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: 256px 256px;
}

/* --- Layout --- */
.container {
  width: 100%;
  max-width: var(--measure-content);
  margin: 0 auto;
  padding: 0 var(--space-4);
}

.container--wide {
  max-width: 1200px;
}

/* --- Pill inputs (the signature shape) --- */
.pill-input {
  display: block;
  width: 100%;
  padding: var(--space-3) var(--space-4-5);
  font-size: var(--fs-base);
  font-family: var(--font-family);
  color: var(--text);
  background: var(--surface);
  border: var(--stroke) solid var(--border);
  border-radius: var(--radius-pill);
  outline: none;
  transition: border-color var(--dur-base) var(--ease-decel);
}

/* Multiline variant of the pill field — a textarea can't be pill-shaped, so it
   takes the card radius and grows vertically. Inherits every other pill-input
   token (padding, colour, border, focus); only geometry changes. */
.pill-input--multiline {
  border-radius: var(--radius-card);
  min-height: calc(var(--space-8) + var(--space-6));
  line-height: 1.5;
  resize: vertical;
}

.pill-input:focus {
  border-color: var(--primary);
}

.pill-input::placeholder {
  color: var(--text-muted);
}

/* --- Buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  font-size: var(--fs-base);
  font-weight: var(--fw-semibold);
  font-family: var(--font-family);
  line-height: 1;
  color: var(--on-primary);
  background: var(--primary);
  border: none;
  border-radius: var(--radius-pill);
  cursor: pointer;
  text-decoration: none;  /* anchor-styled buttons (<a class="btn">) must not underline */
  /* Asymmetric press: base transition is the release spring (transform) plus a
     decelerating fade; :active below overrides the duration to --dur-fast so
     press-in is instant and only the release plays the spring. */
  transition: transform var(--dur-moderate) var(--ease-spring), opacity var(--dur-base) var(--ease-decel);
  -webkit-tap-highlight-color: transparent;
}

.btn:not(:disabled):active {
  transform: scale(0.97);
  opacity: 0.9;
  transition-duration: var(--dur-fast);
}

/* Disabled affordance (M-UI1, final audit): muted + no pointer, same bare-opacity
   pattern as .svc-card--soon (opacity is not a color/length token, ok bare). */
.btn:disabled,
.btn[disabled] {
  opacity: 0.6;
  cursor: default;
}

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

.btn--outline {
  color: var(--primary);
  background: transparent;
  border: var(--stroke) solid var(--primary);
}
.btn--outline:not(:disabled):hover, .btn--outline:not(:disabled):active {
  background: var(--primary);
  color: var(--on-primary);
}
.btn--danger {
  background: var(--danger);
}

.btn--green {
  background: var(--green);
}

.btn--ghost {
  color: var(--text-secondary);
  background: transparent;
  padding: var(--space-2) var(--space-4);
  font-weight: var(--fw-regular);
}

/* --- Card --- */
.card {
  background: var(--surface);
  border-radius: var(--radius-card);
  padding: var(--space-5);
}

.card--link {
  display: block;
  text-decoration: none;
  color: inherit;
  transition: background var(--dur-base) var(--ease-decel);
}

.card--link:hover {
  background: var(--surface-hover);
}

.card-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  margin-bottom: var(--space-1);
}

/* --- Status chip — the ONE status pill, shared across funnel, portal,
   bookings and (as cell fills) the admin roster. Variants map to the
   status token families so the colour language is identical everywhere.
   Translucent fills via color-mix → readable on both light and dark.
   See DESIGN_SYSTEM.md §6 for the vocabulary → variant mapping. */
.status-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  flex-shrink: 0;
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  line-height: 1.4;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}
.status-chip--success { background: color-mix(in srgb, var(--green) 12%, transparent); color: var(--green); }
.status-chip--danger  { background: color-mix(in srgb, var(--coral) 14%, transparent); color: var(--coral); }
.status-chip--info    { background: color-mix(in srgb, var(--info) 14%, transparent);  color: var(--info); }
.status-chip--warning { background: color-mix(in srgb, var(--warning) 20%, transparent); color: var(--warning-text); }
.status-chip--accent  { background: var(--accent-soft); color: var(--primary); }
.status-chip--muted   { background: var(--surface-muted); color: var(--text-secondary); }
/* On dark (customer) surfaces the muted chip needs the dark neutral pair. */
body.dark-page .status-chip--muted { background: var(--dark-surface-strong); color: var(--dark-text-muted); }
body.dark-page .status-chip--warning { color: var(--warning); }

/* --- System notifications: centered modal over a blurred backdrop --- */
.flash-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: var(--scrim);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  animation: flashFadeIn var(--dur-base) ease;
}
.flash-modal {
  width: 100%;
  max-width: 360px;
  background: var(--surface);
  color: var(--text-secondary);
  border-radius: var(--radius-card);
  padding: var(--space-5);
  box-shadow: var(--shadow-modal);
  text-align: center;
  animation: flashPopIn 0.18s ease;
}
body.dark-page .flash-modal {
  background: var(--dark-bg);
  color: var(--dark-text);
  border: var(--hairline) solid var(--dark-border);
}
.flash-modal__msg {
  margin: 0 0 var(--space-4);
  font-size: var(--fs-md);
  line-height: 1.5;
}
.flash-modal__msg--error { color: var(--coral); }
@keyframes flashFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes flashPopIn {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}

/* --- Flash messages (inline — HTMX form errors) --- */
.flash {
  padding: var(--space-3) var(--space-4-5);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  margin-bottom: var(--space-4);
}

.flash--error {
  background: var(--status-alert-bg);
  color: var(--status-alert-text);
  border: var(--hairline) solid var(--status-alert-border);
}

.flash--info {
  background: var(--info-bg);
  color: var(--info-text);
  border: var(--hairline) solid var(--info-border);
}

.flash--success {
  background: var(--status-success-bg);
  color: var(--status-success-text);
  border: var(--hairline) solid var(--status-success-border);
}

/* --- Auth page layout --- */
.auth-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100dvh;
  padding: var(--space-6) var(--space-4);
}

.auth-card {
  width: 100%;
  max-width: 400px;
}

.auth-card h1 {
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  color: var(--text-secondary);
  text-align: center;
  margin-bottom: var(--space-2);
}

.auth-card p {
  text-align: center;
  color: var(--text-muted);
  margin-bottom: var(--space-5);
}

.form-group {
  margin-bottom: var(--space-4);
}

.form-group label {
  display: block;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-secondary);
  margin-bottom: var(--space-1-5);
}

/* --- Dev OTP banner --- */
.dev-otp {
  padding: var(--space-3) var(--space-4-5);
  background: var(--warning-bg-soft);
  border: var(--hairline) solid var(--warning-border);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--n3);
  margin-bottom: var(--space-4);
  text-align: center;
}

.dev-otp code {
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--primary);
  letter-spacing: var(--tracking-widest);
}

/* --- Home page --- */
.home-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-3);
  /* right pad reserves the fixed top-right bell's corner so a long display name
     wraps instead of running under it. */
  padding: var(--space-6) var(--space-8) var(--space-6) 0;
}

/* Persistent notification bell — pinned to the top-right of every cabinet page
   (D37; replaces the bottom-nav bell tab + the old top-right logout icon). Fixed
   so it survives scroll and page changes; theme-adapts (light staff / dark
   customer). Admin/owner don't get it (they use the journal feed). */
.notif-bell {
  position: fixed;
  /* --bell-top is set by app.js to line the bell up with the page title (it
     varies per page and shifts when the pinned banner is shown); fall back to
     the top gutter before that runs / on pages without a measurable title. */
  top: var(--bell-top, var(--space-3));
  right: var(--space-3);
  z-index: var(--z-nav);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  color: var(--text);
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-base) var(--ease-decel), transform var(--dur-moderate) var(--ease-spring);
}
.notif-bell:hover,
.notif-bell:focus-visible,
.notif-bell:active,
.notif-bell--active { color: var(--primary); }
.notif-bell:active { transform: scale(0.92); transition-duration: var(--dur-fast); }
.notif-bell__ic { width: var(--icon-md); height: var(--icon-md); display: block; }
.notif-bell__dot {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  width: var(--space-1-5);
  height: var(--space-1-5);
  border-radius: var(--radius-full);
  background: var(--coral);
}
body.dark-page .notif-bell { color: var(--dark-text); }
body.dark-page .notif-bell:hover,
body.dark-page .notif-bell:focus-visible,
body.dark-page .notif-bell:active { color: var(--primary); }

.home-header h1 {
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--text);
}

.home-header p {
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.role-badge {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--primary);
  border: var(--hairline) solid var(--primary);
  border-radius: var(--radius-pill);
  margin-right: var(--space-1);
  margin-bottom: var(--space-1);
}

/* --- Home page: dark-page (customer) overrides --- */
body.dark-page .home-header h1 {
  color: var(--dark-text);
}
body.dark-page .home-header p {
  color: var(--dark-text-muted);
}
body.dark-page .role-badge {
  color: var(--dark-text-muted);
  border-color: var(--dark-border);
}

/* --- Cabinet: current-booking card (the done-card + reschedule/cancel below) --- */
.cabinet-fol-warn {
  font-size: var(--fs-sm);
  color: color-mix(in srgb, var(--brand) 90%, var(--dark-text));
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-2-5);
  margin-top: var(--space-3);
}

/* --- Utility --- */
.mt-1 { margin-top: var(--space-2); }
.mt-2 { margin-top: var(--space-4); }
.mt-3 { margin-top: var(--space-5); }
.mb-1 { margin-bottom: var(--space-2); }
.mb-2 { margin-bottom: var(--space-4); }
.text-center { text-align: center; }
.text-muted { color: var(--text-muted); }
.text-sm { font-size: var(--fs-sm); }
[x-cloak] { display: none !important; }

/* transitions */
.fade-in  { animation: fadeIn var(--dur-moderate) ease-out; }
.fade-out { animation: fadeIn var(--dur-moderate) ease-out reverse; }
.slide-up   { animation: slideUp var(--dur-slow) ease-out; }
.slide-down { animation: slideUp var(--dur-slow) ease-out reverse; }

@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes slideUp {
  from { opacity: 0; transform: translateY(24px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* --- Modal form elements --- */
.modal-field {
  margin-bottom: var(--space-3-5);
}

.modal-input {
  display: block;
  width: 100%;
  padding: 0.7rem 1.1rem;
  font-size: var(--fs-base);
  font-family: var(--font-family);
  color: var(--on-accent);
  background: transparent;
  border: var(--stroke) solid var(--on-accent-border);
  border-radius: var(--radius-pill);
  outline: none;
  transition: border-color var(--dur-base) var(--ease-decel);
}
.modal-input::placeholder { color: var(--on-accent-faint); }
.modal-input:focus { border-color: var(--on-accent); }

.modal-btn {
  display: block;
  width: 100%;
  padding: 0.7rem;
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
  font-family: var(--font-family);
  color: var(--coral);
  background: var(--white);
  border: none;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: opacity var(--dur-base) var(--ease-decel), transform var(--dur-moderate) var(--ease-spring);
}
.modal-btn:active {
  transform: scale(0.97);
  opacity: 0.9;
  transition-duration: var(--dur-fast);
}

.modal-flash {
  padding: var(--space-2) var(--space-3);
  background: var(--overlay-soft);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  margin-bottom: var(--space-3);
  text-align: center;
}

.modal-dev {
  padding: var(--space-2) var(--space-3);
  background: var(--on-accent-fill);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  margin-bottom: var(--space-3);
  text-align: center;
  letter-spacing: var(--tracking-wider);
}
.modal-dev strong {
  font-size: var(--fs-lg);
  letter-spacing: var(--tracking-widest);
}

.modal-hint {
  font-size: var(--fs-sm);
  text-align: center;
  opacity: 0.75;
  margin-bottom: var(--space-3);
}

.modal-alt {
  text-align: center;
  margin-top: var(--space-3);
}

.modal-link {
  background: none;
  border: none;
  color: var(--on-accent-muted);
  font-size: var(--fs-sm);
  font-family: var(--font-family);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.modal-link:hover { color: var(--on-accent); }

/* ================================================================
   TEACHER AVAILABILITY PAGE (Slice 4)
   ================================================================ */

.avail-header {
  padding: var(--space-5) 0 0;
}

.avail-header h1 {
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
}

.avail-teacher-picker {
  margin-bottom: var(--space-2);
}

.avail-teacher-select {
  width: 100%;
}

.avail-section-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  margin-bottom: var(--space-1);
}

/* --- Week grid (7 rows, toggle + time range) --- */

.week-grid {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Each day is a two-line row: toggle + day name on top, the time range on its
   own full-width line below — so two native time pickers always fit a 375px
   card (the old single-row layout overflowed by ~70px). */
.week-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-2-5) var(--space-3);
  background: var(--bg);
  border-radius: var(--radius-sm);
  transition: opacity var(--dur-base);
}

.week-row--off {
  opacity: 0.7;
}

.week-day {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  flex: 1 1 100%;
}

.week-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  user-select: none;
}

.week-checkbox {
  width: 1.125rem;
  height: 1.125rem;
  accent-color: var(--primary);
  cursor: pointer;
}

.week-day-name {
  font-weight: var(--fw-semibold);
  font-size: var(--fs-base);
}

.week-times {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1 1 100%;
  min-width: 0;
}

/* In the weekly grid the time inputs share the line equally instead of holding
   a fixed 7rem each (which forced the overflow). */
.week-times .pill-input--time {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
}

.week-dash {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  flex-shrink: 0;
}

.week-off-label {
  flex-shrink: 0;
}

.pill-input--time {
  width: 7rem;
  padding: var(--space-2) var(--space-3);
  font-size: var(--fs-sm);
  text-align: center;
}

.pill-input--date {
  width: auto;
  min-width: 9rem;
  padding: var(--space-2) var(--space-3);
  font-size: var(--fs-sm);
}

/* --- Inline forms (dayoff / timeoff add) — stacked full-width on mobile --- */

.avail-inline-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.avail-inline-form .pill-input--date {
  flex: 1 1 100%;
  min-width: 0;
}

.avail-inline-form > .btn {
  flex: 1 1 100%;
}

/* The time-off form keeps start—end together on one line, while date and the
   add button stack full-width above/below it. */
.avail-time-pair {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1 1 100%;
  min-width: 0;
}

.avail-time-pair .pill-input--time {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
}

/* --- Existing entries list --- */

.avail-entries {
  display: flex;
  flex-direction: column;
  gap: var(--space-1-5);
}

.avail-entry {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
}

.avail-entry--dayoff {
  background: var(--status-alert-bg);
  color: var(--status-alert-text);
}

.avail-entry--timeoff {
  background: var(--warning-bg);
  color: var(--warning-text);
}

.avail-entry--override {
  background: var(--status-success-bg);
  color: var(--status-success-text);
}

.avail-del-form {
  display: inline;
}

.btn--sm {
  padding: var(--space-1) var(--space-2);
  font-size: var(--fs-sm);
  min-width: var(--tap-min);
  min-height: var(--tap-min);
}

/* ================================================================
   FREE-LESSON FUNNEL — mobile-first step flow (375px target)
   ================================================================ */

body.dark-page {
  background: var(--dark-bg);
  color: var(--dark-text);
}

/* On dark cards, light-theme greys (--text-muted #9E9E9E / --text-secondary
   #686868) are too dark to read — remap muted/secondary text to the dark-theme
   greys so the consistent dark card stays legible everywhere. */
body.dark-page .text-muted { color: var(--dark-text-muted); }
body.dark-page .auth-card h1 { color: var(--dark-text); }
body.dark-page .auth-card p { color: var(--dark-text-muted); }

body.dark-page .flash--error {
  background: color-mix(in srgb, var(--coral) 12%, transparent);
  color: var(--coral);
  border-color: color-mix(in srgb, var(--coral) 30%, transparent);
}

body.dark-page .flash--info {
  background: color-mix(in srgb, var(--info) 12%, transparent);
  color: var(--info-text-dark);
  border-color: color-mix(in srgb, var(--info) 30%, transparent);
}

.funnel-page {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: var(--dark-bg);
  color: var(--dark-text);
}

.funnel-step {
  position: relative;
  width: 100%;
  max-width: var(--measure-content);
  padding: var(--space-5) var(--space-4) var(--space-6);
  margin: 0 auto;
}

/* Tramplin — the display title (brand scale H3 = --fs-2xl, Bold) */
.funnel-brand {
  display: block;
  text-align: center;
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  line-height: 1;
  color: var(--dark-text);
  text-decoration: none;
  margin: var(--space-1) 0 3.5rem;
  /* Persistent chrome: the wordmark stops re-fading on every step. */
  view-transition-name: funnel-brand;
}

/* Back button — its own row above the title (in flow, so it never collides
   with the large centered wordmark) */
.funnel-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  margin: 0 0 var(--space-1) calc(-1 * var(--space-2));
  color: var(--dark-text-muted);
  text-decoration: none;
  border-radius: var(--radius-full);
  transition: background var(--dur-base) var(--ease-decel);
}
.funnel-back:active {
  background: var(--dark-surface-hover);
}

/* Wireframe head — static. A single fixed SVG can't be 3D-rotated convincingly,
   so progress is shown by brightening it (opacity, set per-step in the markup,
   50%→100% as the bar fills). .funnel-head__in does a one-shot fade-in on load. */
.funnel-head {
  display: flex;
  justify-content: center;
  margin: 6rem 0 3.5rem;
}
/* Review/terminal steps (confirm) carry action buttons, so the head's tall
   default margin leaves the buttons stranded above a big empty band. Tighten
   it on those steps so the actions read as the end of the screen. */
.funnel-head--compact {
  margin: var(--space-6) 0 var(--space-4);
}
.funnel-head__in {
  display: inline-block;
  animation: funnelHeadIn 0.5s ease-out;
  /* Rendered once per page (funnel_step macro) — snapshot separately so the
     head persists across steps and its 50→100% opacity progress BLENDS. */
  view-transition-name: funnel-head;
}
.funnel-head img {
  display: block;
  width: clamp(96px, 28vw, 132px);
  height: auto;
  /* opacity is set inline per step (progress 50%→100%) */
}
@keyframes funnelHeadIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .funnel-head__in { animation: none; }
}

.funnel-progress {
  height: 3px;
  background: var(--dark-border);
  border-radius: var(--radius-2xs);
  margin-bottom: var(--space-5);
  overflow: hidden;
}

.funnel-progress-bar {
  height: 100%;
  background: var(--primary);
  border-radius: var(--radius-2xs);
  transition: width 0.3s ease;
  /* Snapshot separately: the cross-document view transition morphs the old
     width into the new one, so the bar visibly FILLS between steps (the width
     transition above only ever fires within a single document). */
  view-transition-name: funnel-progress;
}

/* Step prompt (the question / greeting). Kept light so the only heavy element
   on the screen is the Tramplin display title — no competing bold blocks. */
.funnel-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-regular);
  color: var(--dark-text);
  margin-bottom: var(--space-4-5);
  line-height: 1.5;
  white-space: pre-line;  /* preserve the bot greeting's paragraph break */
}

.funnel-subtitle {
  font-size: var(--fs-base);
  color: var(--dark-text-muted);
  line-height: 1.5;
  margin-bottom: var(--space-4-5);
}

.funnel-preview + .funnel-subtitle,
.funnel-preview + #confirm-form-body {
  margin-top: var(--space-4-5);
}

.funnel-subtitle--multiline {
  white-space: pre-line;
}

.funnel-body {
  margin-top: var(--space-2);
}

/* --- Pill choice groups --- */

.funnel-pills-form {
  width: 100%;
}

.funnel-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2-5);
}

.funnel-pills--vertical {
  flex-direction: column;
}

.funnel-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3) var(--space-4-5);
  font-size: var(--fs-base);
  font-family: var(--font-family);
  font-weight: var(--fw-semibold);
  color: var(--dark-text);
  background: var(--dark-surface);
  border: var(--stroke) solid var(--dark-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-decel), background var(--dur-base) var(--ease-decel), transform var(--dur-moderate) var(--ease-spring);
  -webkit-tap-highlight-color: transparent;
  min-height: 48px;
  text-align: center;
  text-decoration: none;  /* safe when the pill is an <a> (e.g. «Точно не знаю») */
}

/* Hover = the same solid fill as :active/--active (green + white text on the dark
   funnel, via --primary/--on-primary), not just a green border — the tap-fill
   affordance students get everywhere. */
.funnel-pill:hover {
  background: var(--primary);
  color: var(--on-primary);
  border-color: var(--primary);
}

.funnel-pill:active {
  transform: scale(0.97);
  background: var(--primary);
  color: var(--on-primary);
  border-color: var(--primary);
  transition-duration: var(--dur-fast);
}

.funnel-pill--active {
  background: var(--primary);
  color: var(--on-primary);
  border-color: var(--primary);
}

.funnel-pill--wide {
  width: 100%;
  text-align: left;
  justify-content: flex-start;
}

/* --- Day strip (horizontal scroll) --- */

.funnel-day-strip {
  display: flex;
  gap: var(--space-1-5);
  overflow-x: auto;
  padding-bottom: var(--space-2);
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.funnel-day-strip::-webkit-scrollbar { display: none; }

.funnel-day {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 56px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-family: var(--font-family);
  color: var(--dark-text-muted);
  background: var(--dark-surface);
  border: var(--stroke) solid transparent;
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-decel), background var(--dur-base) var(--ease-decel);
  -webkit-tap-highlight-color: transparent;
}

/* Hover fills the day green + white text (the same tap-fill as --active), matching
   the pills/time-chips — the funnel's student-facing selection buttons all fill on
   hover, never a border. */
.funnel-day:hover {
  background: var(--primary);
  color: var(--on-primary);
  border-color: var(--primary);
}

.funnel-day--active {
  background: var(--primary);
  color: var(--on-primary);
  border-color: var(--primary);
}

/* Weekend coral applies only when the day is neither selected nor hovered — a
   hovered/selected day fills green and takes white text (like the --active exclusion). */
.funnel-day--weekend:not(.funnel-day--active):not(:hover) {
  color: var(--coral);
}

/* Days with no free slots in the horizon — dimmed so the visitor reaches for
   an open day first (still clickable; shows the per-day empty message). */
.funnel-day--empty:not(.funnel-day--active) {
  opacity: 0.35;
}

.funnel-day-weekday {
  font-size: var(--fs-xs);
  text-transform: lowercase;
}

.funnel-day-num {
  font-weight: var(--fw-bold);
  font-size: var(--fs-base);
  line-height: 1;
}

/* --- Time chips grid --- */

.funnel-time-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: var(--space-2);
}

.funnel-time-chip {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-2-5) var(--space-2);
  font-size: var(--fs-base);
  font-family: var(--font-family);
  font-weight: var(--fw-semibold);
  color: var(--dark-text);
  background: var(--dark-surface);
  border: var(--stroke) solid var(--dark-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-decel), background var(--dur-base) var(--ease-decel), transform var(--dur-moderate) var(--ease-spring);
  -webkit-tap-highlight-color: transparent;
  min-height: var(--tap-min);
  white-space: nowrap;   /* keep "⚡ 19:30" on one line — never wrap into a blob */
}

.funnel-time-chip:hover {
  background: var(--primary);
  color: var(--on-primary);
  border-color: var(--primary);
}

.funnel-time-chip:active {
  transform: scale(0.96);
  background: var(--primary);
  color: var(--on-primary);
  border-color: var(--primary);
  transition-duration: var(--dur-fast);
}

/* hide per-chip hidden inputs from layout */
.funnel-time-chip input[type="hidden"] {
  display: none;
}

/* Swap settle: freshly inserted content materializes (fade + small rise)
   instead of popping in the same frame. Keyframes fire on node insertion, so
   the initial server render gets the same gesture as an HTMX swap. */
@keyframes swap-in { from { opacity: 0; transform: translateY(var(--space-1)); } }

/* Time chips fade in SEQUENTIALLY (opacity only — no vertical travel, so
   switching dates never jitters the block). Each chip carries `--chip-i` (its
   position, set in _time_chips.html); the delay is that index × the stagger
   step, capped at 8 so a long list still finishes promptly. `both` holds the
   pre-delay opacity:0 so nothing flashes before its turn. */
@keyframes chip-in { from { opacity: 0; } }
#time-chips .funnel-time-chip {
  animation: chip-in var(--dur-moderate) var(--ease-decel) both;
  animation-delay: calc(min(var(--chip-i, 0), 8) * var(--dur-stagger));
}

/* ⚡ pinned "quick" slots use the normal grey chip style — only the ⚡ marks them
   (an accent fill read as "pre-selected"). */
.funnel-chip-bolt {
  margin-right: var(--space-1);
  line-height: 1;
}

.funnel-slot-note {
  font-size: var(--fs-sm);
  color: var(--dark-text-muted);
  margin-bottom: var(--space-2);
}

/* Fine-print disclosure below the ⚡ quick slots. The sentence is quiet grey
   fine print; only «другое время.» inside it is the underlined inline text link. */
.funnel-more-prompt {
  margin-top: var(--space-5);
  font-size: var(--fs-sm);
  color: var(--dark-text-muted);
}
.funnel-more-toggle {
  padding: 0;
  background: none;
  border: 0;
  font: inherit;                       /* match the surrounding fine print */
  color: inherit;
  text-decoration: underline;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-base) var(--ease-decel);
}
.funnel-more-toggle:hover { color: var(--dark-text); }

/* Dismiss the WHOLE message (not just the link word): fade + small rise out,
   then JS sets [hidden] on the prompt paragraph. */
@keyframes more-prompt-out { to { opacity: 0; transform: translateY(calc(-1 * var(--space-1))); } }
.funnel-more-prompt--out { animation: more-prompt-out var(--dur-base) var(--ease-decel) forwards; }

/* The hidden ordinary grid. It owns the reveal motion as one block (fade in from
   below), so its chips do NOT run the per-chip stagger (overrides the id-scoped
   #time-chips .funnel-time-chip rule — needs #time-chips to out-specify it). */
@keyframes more-reveal { from { opacity: 0; transform: translateY(var(--space-2)); } }
.funnel-more--in { animation: more-reveal var(--dur-moderate) var(--ease-decel) both; }
#time-chips .funnel-more .funnel-time-chip { animation: none; }

.funnel-more-sep {
  height: 0;
  margin: var(--space-3) 0;
  border-top: var(--hairline) solid var(--dark-border);
}

/* --- Funnel empty / error states --- */

.funnel-empty {
  text-align: center;
  color: var(--dark-text-muted);
  font-size: var(--fs-base);
  padding: var(--space-6) var(--space-4);
}

/* --- Carousel (teacher bio, slice 9 + discipline explainer, §8) --- */

/* Progressive enhancement: hidden until the carousel core (carousel.js) marks
   the carousel ready; without JS the stack + native audio + Select still work. */
/* Scoped to the carousel so it can't hide unrelated elements elsewhere. */
/* SHARED OWNERSHIP — these comma-grouped rules style BOTH the teacher carousel
   (.tcar/.tcard) and the discipline carousel (.dcar/.dcard); they share the
   carousel.js paging engine too. A change here affects BOTH. To restyle only
   one, SPLIT the selector (move that carousel's prefix to its own rule) — do not
   edit the shared rule in place. The `__content` classes (.tcard__photo,
   .dcard__caption, …) are per-carousel and live in their own blocks below. */
.tcar .js-only,
.dcar .js-only { display: none; }
.tcar--ready .js-only,
.dcar--ready .js-only { display: flex; }

.tcar,
.dcar {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  width: 100%;
  /* Breathing room so the sticky nav pill settles below the card with a gap
     and never rests over the Выбрать CTA (F3). */
  padding-bottom: var(--space-8);
}

.tcard,
.dcard {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  background: var(--dark-card);
  border: var(--stroke) solid var(--dark-border);
  border-radius: var(--radius-card);
  padding: var(--space-4);
}

/* When the carousel is live, show ONE card but CROSSFADE between them. All cards
   stack in the same spot: inactive ones are absolutely-positioned + transparent,
   the active one stays in flow so it drives the container height. Paging fades
   the outgoing card out while the incoming fades in — both are rendered through
   the whole transition, so there is never an empty (black) frame between cards
   (a display:none swap left a gap while the next card faded up from nothing).
   `visibility` is held until the fade finishes so a hidden card leaves the a11y
   tree and can't be tabbed to. */
.tcar--ready,
.dcar--ready { position: relative; }
.tcar--ready .tcard,
.dcar--ready .dcard {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--dur-slowest) var(--ease-decel),
              visibility 0s linear var(--dur-slowest);
}
.tcar--ready .tcard.is-active,
.dcar--ready .dcard.is-active {
  position: relative;
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity var(--dur-slowest) var(--ease-decel);
}

/* Direction-explainer card content (FUNNEL_REDESIGN §8). */
.dcard__photo {
  width: 100%;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--dark-inset);
}
.dcard__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.dcard__caption {
  margin: 0;
  color: var(--dark-text);
  line-height: 1.5;
}
.dcard__select { margin-top: var(--space-2); }

.tcard__photo {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--dark-inset);
}

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

/* Neutral placeholder when a teacher has no photo (initial on the grey panel). */
.tcard__photo--ph {
  display: flex;
  align-items: center;
  justify-content: center;
}

.tcard__photo--ph span {
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  color: var(--dark-text-muted);
  text-transform: uppercase;
  line-height: 1;
}

.tcard__name {
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--dark-text);
  margin: 0;
  line-height: 1.2;
}

.tcard__meta {
  font-size: var(--fs-sm);
  color: var(--dark-text-muted);
  margin: var(--space-1) 0 0;
}

.tcard__pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1-5);
}

.tcard__pill {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--primary);
  border: var(--stroke) solid var(--primary);
  border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-2-5);
}

.tcard__bio {
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--dark-text);
  white-space: pre-line;        /* preserve the bot bio's paragraph breaks */
}

.tcard__courses {
  font-size: var(--fs-sm);
  color: var(--dark-text);
  line-height: 1.5;
  margin: 0;
}

.tcard__label {
  color: var(--dark-text-muted);
  font-weight: var(--fw-semibold);
}

.tcard__genres {
  font-size: var(--fs-sm);
  color: var(--primary);
  font-weight: var(--fw-semibold);
  margin: 0;
}

.tcard__select { margin-top: var(--space-1); }

/* --- Minimal audio player --- */

.tcard__player {
  background: var(--dark-overlay);
  border-radius: var(--radius-card);
  padding: var(--space-3);
}

.tcard__audio { width: 100%; }       /* native fallback (no-JS) */

.tplayer { flex-direction: column; gap: var(--space-2); }

.tplayer__title {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--dark-text);
  text-align: center;
}

.tplayer__seekrow {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.tplayer__cur, .tplayer__dur {
  font-size: var(--fs-xs);
  color: var(--dark-text-muted);
  font-variant-numeric: tabular-nums;
  min-width: 2.5rem;
  text-align: center;
}

.tplayer__seek {
  flex: 1;
  accent-color: var(--primary);
  cursor: pointer;
  height: 4px;
}

.tplayer__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-5);
}

.tplayer__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  color: var(--dark-text);
  background: none;
  border: none;
  cursor: pointer;
  border-radius: var(--radius-full);
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--dur-moderate) var(--ease-spring), color var(--dur-base) var(--ease-decel);
}

/* Transport icons are inlined from assets/Teacher card/Player.svg (currentColor
   so the dark theme controls colour). All transport glyphs are 20px (--icon-sm)
   — prev/next via the base rule, play via the .tplayer__play override below. */
.tplayer__ic {
  width: var(--icon-sm);
  height: var(--icon-sm);
  display: block;
  fill: currentColor;
}

.tplayer__btn:hover { color: var(--primary); }
.tplayer__btn:active { transform: scale(0.9); transition-duration: var(--dur-fast); }

.tplayer__play {
  background: var(--primary);
  color: var(--on-primary);
  min-width: 52px;
  min-height: 52px;
}

.tplayer__play:hover { color: var(--on-primary); }
.tplayer__play .tplayer__ic { width: var(--icon-sm); height: var(--icon-sm); }
/* Optical centering: a right-pointing triangle reads as centred only when
   nudged slightly toward its apex. (The pause bars are symmetric — no nudge.) */
.tplayer__play .tplayer__ic--play { transform: translateX(1.5px); }

/* Play/pause toggle: swap icons via .is-playing on the play button. */
.tplayer__play .tplayer__ic--pause { display: none; }
.tplayer__play.is-playing .tplayer__ic--play { display: none; }
.tplayer__play.is-playing .tplayer__ic--pause { display: block; }

/* --- Carousel nav (prev / N-of-M / next) --- */

/* Sticky bottom bar: the nav is the last child after the (possibly tall) card,
   so `position: sticky; bottom` floats it just above the viewport bottom while
   the card overflows — always in thumb reach — and lets it settle naturally
   below the card once its end scrolls into view. No effect when the card fits.
   Rendered as a centred translucent pill so it reads over card content. */
/* Shared floating-pill chrome (D36): the sticky frosted-glass bottom pill, used
   by the carousel navs AND the customer bottom-nav (one rule → one design).
   Apple-style vibrancy: translucent (--dark-nav-bg @ 58%) + heavy blur/saturation
   + a light glass edge + soft depth. No `display` here on purpose — the carousels
   render as a centred block (fit-content + margin auto) with inline children; the
   bottom-nav opts into grid via its own class below. */
.floating-nav,
.tcar__nav,
.dcar__nav {
  position: sticky;
  bottom: var(--space-3);
  z-index: var(--z-raised);
  width: fit-content;
  margin: var(--space-2) auto 0;
  padding: var(--space-1-5);
  border-radius: var(--radius-pill);
  border: var(--hairline) solid var(--dark-surface-strong);
  background: var(--dark-nav-bg);
  backdrop-filter: blur(22px) saturate(180%);
  -webkit-backdrop-filter: blur(22px) saturate(180%);
  box-shadow: var(--elevation-4);
}

/* Carousel nav keeps its own centring declarations — identical computed style to
   before the .floating-nav split (no behaviour change). */
.tcar__nav,
.dcar__nav {
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
}

.tcar__arrow,
.dcar__arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  font-size: var(--fs-xl);
  line-height: 1;
  color: var(--dark-text);
  background: var(--dark-surface);
  border: var(--stroke) solid var(--dark-border);
  border-radius: var(--radius-full);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: border-color var(--dur-base) var(--ease-decel), transform var(--dur-moderate) var(--ease-spring);
}

.tcar__arrow:hover,
.dcar__arrow:hover { border-color: var(--primary); }
.tcar__arrow:active,
.dcar__arrow:active { transform: scale(0.92); transition-duration: var(--dur-fast); }

.tcar__chev,
.dcar__chev { width: var(--icon-md); height: var(--icon-md); display: block; }

.tcar__indicator,
.dcar__indicator {
  font-size: var(--fs-sm);
  color: var(--dark-text-muted);
  font-variant-numeric: tabular-nums;
  min-width: 3.5rem;
  text-align: center;
}

/* --- Cabinet buttons (D36) — calm neutral grey by default; the brand accent
   (green on the customer theme, via --primary) only on hover / press. The
   customer cabinet reads quiet, with the accent as the interaction highlight
   (the funnel keeps its accent-by-default convention — this is scoped to
   body.cabinet-page). --- */
body.cabinet-page .btn {
  color: var(--dark-text-muted);
  background: transparent;
  border: var(--stroke) solid var(--dark-border);
  transition: color var(--dur-base) var(--ease-decel), border-color var(--dur-base) var(--ease-decel),
              background-color var(--dur-base) var(--ease-decel), transform var(--dur-moderate) var(--ease-spring);
}
/* Hover / focus / pressed: the pill fills with the brand green and the label goes
   white (solid fill, not a faint tint) — the clear "selected" state. */
body.cabinet-page .btn:not(:disabled):hover,
body.cabinet-page .btn:not(:disabled):focus-visible,
body.cabinet-page .btn:not(:disabled):active {
  color: var(--on-primary);
  border-color: var(--primary);
  background: var(--primary);
}

/* --- Customer bottom nav (D36) — floating frosted-glass pill of icon+caption
   tabs (glass chrome inherited from .floating-nav above). Symmetrical
   equal-width columns (grid 1fr per tab → all tabs size to the widest), with
   hairline dividers between them. The column count follows the tab count
   (4 since D40 added the Чат tab); on a narrow phone the pill caps at the
   viewport width and the equal columns shrink to fit a single row. */
.bottom-nav {
  display: flex;
  flex-wrap: nowrap;            /* a flex row never wraps — the four tabs stay on one line */
  align-items: stretch;
  /* Fill the width up to a compact cap rather than shrink-to-fit: a fit-content
     pill came out too narrow and truncated the captions. 420px stays a tidy
     centred pill on desktop; on a phone it fills (minus the side gutter) so all
     four equal tabs show their captions in full. (Layout width — the px cap is a
     DESIGN_SYSTEM §6f container-width exception, as elsewhere in this file.) */
  width: min(420px, calc(100vw - var(--space-4)));
}

.bottom-nav__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  flex: 1 1 0;                  /* equal-width tabs that shrink to fit one row */
  min-width: 0;                /* allow shrinking below caption width (truncates) */
  min-height: var(--tap-min);
  padding: var(--space-1);     /* tight horizontal padding so the longest caption («Расписание») fits */
  color: var(--dark-text-muted);
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-base) var(--ease-decel);
}

/* Divider between adjacent tabs. */
.bottom-nav__item + .bottom-nav__item {
  border-left: var(--hairline) solid var(--dark-border);
}

.bottom-nav__item:hover { color: var(--dark-text); }
.bottom-nav__item--active { color: var(--primary); }

.bottom-nav__ic { width: var(--icon-md); height: var(--icon-md); display: block; flex: none; }
.bottom-nav__cap { font-size: var(--fs-xs); line-height: 1; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Unread badge on a nav tab (D40 chat). Anchored to the icon; brand-accent pill
   (--primary — green on the customer theme) with light text, hidden at zero.
   chat.js flips [hidden] + count live. */
.bottom-nav__ic-wrap { position: relative; display: block; }
.bottom-nav__badge {
  position: absolute;
  top: calc(-1 * var(--space-1-5));
  inset-inline-end: calc(-1 * var(--space-2-5));
  min-width: var(--space-4-5); height: var(--space-4-5); padding: 0 var(--space-1);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--primary); color: var(--surface);
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); line-height: 1;
  border-radius: var(--radius-pill);
}
.bottom-nav__badge[hidden] { display: none; }

/* Teacher cabinet nav (D37) — light re-skin of the shared floating glass (the
   customer nav is dark; admin/teacher surfaces are light, AGENTS §6a). Loaded
   from app.css (not teacher.css) because the teacher nav renders on every teacher
   page, including ones that don't load teacher.css. Same layout, light tokens. */
.teacher-nav {
  background: var(--surface);
  border-color: var(--border);
  box-shadow: var(--shadow-popover);
}
.teacher-nav .bottom-nav__item { color: var(--text-muted); }
.teacher-nav .bottom-nav__item:hover { color: var(--text); }
.teacher-nav .bottom-nav__item--active { color: var(--primary); }
.teacher-nav .bottom-nav__item + .bottom-nav__item { border-left-color: var(--border); }

/* Reserve clearance at the bottom of cabinet pages so the floating nav never
   settles on top of the last tap targets (reschedule / cancel / save). The pill
   floats within this padding instead of over content. */
body.cabinet-page .container,
body.cabinet-page .contacts-page,
body.cabinet-page .profile-page,
body.cabinet-page .mb-page {
  padding-bottom: var(--space-8);
}

/* Customer pages run on the dark theme, but .card defaults to the light
   --surface (#fff) → light text on white = invisible. Every customer-facing
   card (funnel confirm/done, login, account home, «Мои записи») gets ONE
   consistent dark elevated surface matching the day-strip / time chips. Admin
   /teacher surfaces are light (no .dark-page), so they keep the white card. */
body.dark-page .card {
  background: var(--dark-card);
  border: var(--hairline) solid var(--dark-border);
}

/* --- Confirm preview --- */

.funnel-preview {
  line-height: 1.5;
}
.funnel-preview__lead {
  margin: 0 0 var(--space-4-5);
  color: var(--dark-text);
}
.funnel-facts {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
}
.funnel-fact {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}
.funnel-fact dt {
  color: var(--dark-text-muted);
  font-size: var(--fs-sm);
  flex-shrink: 0;
}
.funnel-fact dd {
  margin: 0;
  color: var(--dark-text);
  font-weight: var(--fw-semibold);
  text-align: right;
}
.funnel-preview__q {
  margin: var(--space-4-5) 0 0;
  color: var(--dark-text);
}
/* A link painted in the theme's accent. On dark pages a bare <a> renders in the
   UA's blue, which is off-brand and jarring against the green accent; the token
   is the READABLE green (--green-web itself is a fill colour and fails AA as
   text — see tokens.css). Shared, because every dark surface has this problem. */
.link-accent { color: var(--accent); }
body.dark-page .link-accent { color: var(--accent-on-dark); }

/* --- Studio address block (confirm + done, FUNNEL_REDESIGN §7) --- */
/* The address rows reuse .funnel-facts; a hairline rule sets them apart from the
   booking facts above. The extra line + online note are muted full-width copy. */
.funnel-addr {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--hairline) solid var(--dark-border);
}
.funnel-addr-extra,
.funnel-addr-online {
  margin: 0;
}

/* --- Done / success --- */

/* The structured done card is left-aligned (labelled facts + address); only the
   fallback + temp-pw blocks centre. */
.funnel-done {
  line-height: 1.5;
}
.funnel-done__reminder {
  margin: var(--space-4-5) 0 0;
}

.funnel-success {
  text-align: center;
}
.funnel-success p {
  margin: 0;
}

/* One-time temporary password on the done screen (FUNNEL_REDESIGN §3.5) */
.funnel-temp-pw {
  text-align: center;
}
.funnel-temp-pw p {
  margin: 0;
}
.funnel-temp-pw__code code {
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  color: var(--primary);
  letter-spacing: var(--tracking-wider);
}

.funnel-page .btn--ghost {
  color: var(--dark-text-muted);
}

.funnel-page .btn--outline {
  border-color: var(--dark-border);
  color: var(--dark-text);
}

.funnel-page .pill-input {
  background: var(--dark-surface);
  color: var(--dark-text);
  border-color: var(--dark-border);
}

.funnel-page .pill-input::placeholder {
  color: var(--dark-text-muted);
}

.funnel-page label {
  color: var(--dark-text-muted);
}

/* «(необязательно)» hint inside a label — lighter + smaller than the label. */
.funnel-page .form-hint {
  font-size: var(--fs-sm);
  font-weight: var(--fw-regular);
  opacity: 0.7;
}

/* Confirm-screen action group — primary + secondary stacked as one block with
   a consistent gap (replaces the ad-hoc mt-3/mt-2 on the two buttons). */
.funnel-actions {
  margin-top: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2-5);
}

.funnel-page .flash--error {
  color: var(--coral);
}

/* P2-16: HTMX loading indicator */
.htmx-request .btn,
.htmx-request button[type="submit"] {
  opacity: 0.6;
  pointer-events: none;
}
/* Non-.btn submit buttons (e.g. .funnel-pill) snapped straight to the dimmed
   state. Scope the fade to the in-flight state ONLY — an unscoped
   button[type="submit"]:not(.btn) rule outranks .funnel-pill's own transition
   (specificity 0,0,2,1 > 0,0,1,0) and would clobber the pill's spring press.
   Pills use plain form POST, so they never carry .htmx-request. */
.htmx-request button[type="submit"]:not(.btn) { transition: opacity var(--dur-base) var(--ease-decel); }

.htmx-error {
  animation: htmxFadeIn var(--dur-moderate) ease;
}

@keyframes htmxFadeIn {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Top banner section (D40 §7): a full-width strip at the very top of the page.
   Pinned (sticky) so it stays put as the page scrolls — that keeps the fixed
   notification bell tucked to the title band instead of drifting over content
   once the banner would otherwise scroll away. It still takes its space in flow
   (pushes content down), so nothing overlaps at the top. Brand-accent fill
   (--primary — green on the customer theme), white text. Reusable for
   announcements by swapping the text. Tokens only (DESIGN_SYSTEM §0c). */
.app-banner {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky-header);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2-5) var(--space-4);
  padding-top: calc(var(--space-2-5) + env(safe-area-inset-top, 0px));
  background: var(--primary);
  color: var(--on-primary);
}
.app-banner[hidden] { display: none; }
.app-banner__text {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--fs-sm);
  color: var(--on-primary);
}
/* Single enable button: a high-contrast white pill on the accent bar. */
.app-banner__btn {
  flex: none;
  padding: var(--space-1-5) var(--space-4);
  border: var(--stroke) solid var(--on-primary);
  border-radius: var(--radius-pill);
  background: var(--on-primary);
  color: var(--primary);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity var(--dur-base) var(--ease-decel), transform var(--dur-moderate) var(--ease-spring);
}
.app-banner__btn:hover { opacity: 0.92; }
.app-banner__btn:active { transform: scale(0.97); transition-duration: var(--dur-fast); }
.app-banner__btn:disabled { opacity: 0.6; cursor: default; }
.app-banner__btn[hidden] { display: none; }

/* ── Waitcall dial screen (D42 Pass 3) ─────────────────────────────────
   Shared by the three anonymous doors (login, funnel confirm, shop checkout).

   Rebuilt 2026-07-22 to stop inventing. The number is a `.funnel-facts` row in
   a `.card`, prose is `.funnel-subtitle` / `.form-hint`, the action is `.btn` —
   all catalog components, so the screen inherits the funnel's type scale and
   alignment instead of declaring its own. What remains below is only what the
   system genuinely lacks: a waiting indicator and the status row it sits in. */
.waitcall { animation: swap-in var(--dur-moderate) var(--ease-decel); }
/* The card holds nothing but the number, so the number fills it: no label to
   compete with, sized at the display step (--fs-xl, the same step the funnel
   already uses for its largest type — not a new one). Block + padding puts the
   tap target well over the 44px floor (DESIGN_SYSTEM §0a), which the number
   needs more than anything else here: it is what a phone actually taps. */
/* The same gap `.funnel-preview + .funnel-subtitle` gives the confirm screens —
   this card is not a `.funnel-preview`, so it does not inherit that adjacency
   rule and would otherwise sit flush against the sentence below it. */
.waitcall__card {
  text-align: center;
  margin-bottom: var(--space-4-5);
}
.waitcall__number {
  display: block;
  /* Just enough to clear the 44px tap floor (DESIGN_SYSTEM §0a) — the card's own
     padding supplies the breathing room, so anything more double-pads it. */
  padding: var(--space-1) 0;
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  color: var(--dark-text);
  text-decoration: none;
  /* The one string on this screen that must never wrap or overflow its card. */
  white-space: nowrap;
}
.waitcall__state {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  color: var(--dark-text-muted);
  font-size: var(--fs-sm);
}
/* Three dots pulsing in sequence — "we are waiting with you", not "the page is
   loading". Opacity only (AGENTS §6), staggered by thirds of the shared pulse
   token so no new duration enters the scale. The dot size is a component-scoped
   custom property: DESIGN_SYSTEM §6f authorises those, so this needs no
   css_audit baseline entry. */
.waitcall__dots {
  --waitcall-dot: 6px;
  display: inline-flex;
  gap: var(--space-1);
}
.waitcall__dots i {
  width: var(--waitcall-dot);
  height: var(--waitcall-dot);
  border-radius: 50%;
  background: currentColor;
  animation: waitcallDot var(--dur-pulse) var(--ease-standard) infinite;
}
.waitcall__dots i:nth-child(2) { animation-delay: calc(var(--dur-pulse) / 3); }
.waitcall__dots i:nth-child(3) { animation-delay: calc(var(--dur-pulse) / 3 * 2); }
@keyframes waitcallDot { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }
/* The retry link is a real button element (it posts) styled as a quiet link —
   .btn--ghost would give it a control's weight, which it should not have. */
.waitcall__retry {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--dark-text-muted);
  font-size: var(--fs-sm);
  text-decoration: underline;
}
/* .btn sets display:inline-flex, which BEATS the [hidden] attribute's UA
   display:none — without this the «Я позвонил» button and the hidden state
   blocks would all be visible at once. Same fix as .app-banner__btn[hidden].
   .waitcall__state is display:flex and needs exactly the same protection. */
.waitcall [hidden] { display: none; }

/* Reduced motion — gentler equivalents, not zero feedback: travel/spring
   animations and the cross-document transition are dropped; short colour/
   opacity state transitions and press feedback stay (non-vestibular). */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none; }
  .flash-overlay, .flash-modal, .htmx-error,
  .fade-in, .fade-out, .slide-up, .slide-down,
  #time-chips .funnel-time-chip, .waitcall,
  .funnel-more--in, .funnel-more-prompt--out { animation: none; }
  /* The waiting dots stop pulsing but stay visible — a static three-dot mark is
     still "we are waiting", whereas hiding them would leave the screen looking
     inert during the one wait that most needs to look alive. */
  .waitcall__dots i { animation: none; }
  /* Carousel now crossfades via transition, not animation — cut it to an
     instant swap (still no black frame: the incoming card is opaque at once). */
  .tcar--ready .tcard, .dcar--ready .dcard,
  .tcar--ready .tcard.is-active, .dcar--ready .dcard.is-active { transition: none; }
}
