/*
 * Copyright (c) 2026 QuickStudio LLC. All rights reserved.
 * Proprietary and confidential. See the LICENSE file at the repository root.
 * Unauthorized copying, distribution or use, in any medium, is prohibited.
 */

/* QuickStudio Web Interface - Clean, Simple, Functional */

:root {
  /* ============================================
     Brand Colors (constant across themes)
     ============================================ */
  --primary-color: #2563eb;
  --primary-dark: #1e40af;
  --primary-light: #dbeafe;
  --secondary-color: #10b981;
  --danger-color: #ef4444;
  --warning-color: #f59e0b;

  /* ============================================
     Base Gray Scale (constant)
     ============================================ */
  --gray-50: #f9fafb;
  --gray-100: #f3f4f6;
  --gray-200: #e5e7eb;
  --gray-300: #d1d5db;
  --gray-400: #9ca3af;
  --gray-500: #6b7280;
  --gray-600: #4b5563;
  --gray-700: #374151;
  --gray-800: #1f2937;
  --gray-900: #111827;

  /* ============================================
     Semantic Theme Variables (Light Mode Default)
     These are overridden by [data-theme="dark"]
     ============================================ */
  --bg-page: var(--gray-50);
  --bg-card: white;
  --bg-card-alt: var(--gray-100);
  --bg-input: white;
  --bg-hover: var(--gray-50);
  --bg-table-header: var(--gray-100);

  --text-primary: var(--gray-900);
  --text-secondary: var(--gray-700);
  --text-muted: var(--gray-500);
  --text-on-primary: white;

  --border-color: var(--gray-200);
  --border-color-strong: var(--gray-300);

  --sidebar-bg: var(--gray-900);
  --sidebar-text: rgba(255, 255, 255, 0.8);
  --sidebar-text-hover: white;
  --sidebar-border: rgba(255, 255, 255, 0.1);

  --modal-backdrop: rgba(0, 0, 0, 0.5);

  /* ============================================
     Semantic Status Colors (Light Mode)
     Used by status badges, alerts, and inline status pills.
     Soft background + accessible strong text per tone; warning also
     ships a border (used by inline panels like proposed-slot-fields).
     ============================================ */
  --bg-success-soft: #dcfce7;
  --text-success-strong: #166534;

  --bg-warning-soft: #fef3c7;
  --text-warning-strong: #92400e;
  --border-warning: #fbbf24;

  --bg-info-soft: #dbeafe;
  --text-info-strong: #0c4a6e;

  --bg-danger-soft: #fee2e2;
  --text-danger-strong: #991b1b;

  --bg-neutral-soft: #f3f4f6;
  --text-neutral-strong: #6b7280;

  --link-color: #667eea;

  /* Stronger button-hover variants of brand colors */
  --danger-color-hover: #dc2626;
  --secondary-color-hover: #059669;

  /* Group-event accent. The soft fill and the solid fill both flip in dark
     mode; --group-fill stays dark enough for white text. */
  --group-accent: #7c3aed;
  --group-accent-strong: #6d28d9;
  --group-accent-soft: #faf5ff;
  --group-accent-hover: #ede9fe;
  --group-fill: #7c3aed;

  /* ============================================
     Font Sizes (can be scaled by data-font-size)
     ============================================ */
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.25rem;
  --font-size-2xl: 1.5rem;
  --font-size-3xl: 2rem;

  /* ============================================
     Spacing & Effects (constant)
     ============================================ */
  --border-radius: 8px;
  --transition: all 0.2s ease;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

/* ============================================
   DARK THEME
   ============================================ */
[data-theme='dark'] {
  --bg-page: #0f172a;
  --bg-card: #1e293b;
  --bg-card-alt: #334155;
  --bg-input: #1e293b;
  --bg-hover: #334155;
  --bg-table-header: #1e293b;

  --text-primary: #f8fafc;
  --text-secondary: #cbd5e1;
  --text-muted: #94a3b8;
  --text-on-primary: white;

  --border-color: #475569;
  --border-color-strong: #64748b;

  --sidebar-bg: #020617;
  --sidebar-text: rgba(255, 255, 255, 0.9);
  --sidebar-text-hover: white;
  --sidebar-border: rgba(255, 255, 255, 0.15);

  --modal-backdrop: rgba(0, 0, 0, 0.7);

  /* Adjust primary-light for dark mode contrast */
  --primary-light: #1e40af;

  /* Shadows need more opacity in dark mode */
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2);

  /* ============================================
     Semantic Status Colors (Dark Mode)
     Soft tints become translucent overlays of the brand color so they
     stay legible against the dark card background. Strong text shifts
     to lighter tints for contrast.
     ============================================ */
  --bg-success-soft: rgba(16, 185, 129, 0.18);
  --text-success-strong: #6ee7b7;

  --bg-warning-soft: rgba(245, 158, 11, 0.18);
  --text-warning-strong: #fcd34d;
  --border-warning: rgba(245, 158, 11, 0.55);

  --bg-info-soft: rgba(59, 130, 246, 0.18);
  --text-info-strong: #93c5fd;

  --bg-danger-soft: rgba(239, 68, 68, 0.18);
  --text-danger-strong: #fca5a5;

  --bg-neutral-soft: rgba(148, 163, 184, 0.18);
  --text-neutral-strong: #cbd5e1;

  --link-color: #a5b4fc;

  --danger-color-hover: #f87171;
  --secondary-color-hover: #34d399;

  --group-accent: #c4b5fd;
  --group-accent-strong: #a78bfa;
  --group-accent-soft: #4c1d95;
  --group-accent-hover: #5b21b6;
  --group-fill: #5b21b6;
}

[data-theme='dark'] .avatar-dropdown-item.danger:hover {
  background: var(--bg-danger-soft);
}

/* ============================================
   UI FONT CHOICE (UI-FONT-CHOICE-01)
   Self-hosted Latin-subset WOFF2 (CSP font-src 'self'). A face is only
   fetched when a rule using it matches, so only the chosen font loads.
   theme.js sets data-font-family on <html> only while feature.font_choice
   is on.
   ============================================ */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../vendor/fonts/inter/inter-latin-wght-normal.woff2') format('woff2');
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../vendor/fonts/montserrat/montserrat-latin-wght-normal.woff2') format('woff2');
}

@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../vendor/fonts/roboto/roboto-latin-400-normal.woff2') format('woff2');
}

@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../vendor/fonts/roboto/roboto-latin-500-normal.woff2') format('woff2');
}

@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../vendor/fonts/roboto/roboto-latin-700-normal.woff2') format('woff2');
}

html[data-font-family='inter'],
html[data-font-family='inter'] body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
}

html[data-font-family='roboto'],
html[data-font-family='roboto'] body {
  font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
}

html[data-font-family='montserrat'],
html[data-font-family='montserrat'] body {
  font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
}

html[data-font-family] button,
html[data-font-family] input,
html[data-font-family] select,
html[data-font-family] textarea {
  font-family: inherit;
}

/* ============================================
   LARGE FONT MODE (Accessibility)
   ============================================ */
[data-font-size='large'] {
  --font-size-xs: 0.875rem;
  --font-size-sm: 1rem;
  --font-size-base: 1.125rem;
  --font-size-lg: 1.25rem;
  --font-size-xl: 1.5rem;
  --font-size-2xl: 1.75rem;
  --font-size-3xl: 2.25rem;
}

/* ============================================
   HIGH CONTRAST MODE (Accessibility)
   ============================================ */
[data-high-contrast='true'] {
  --text-primary: #000000;
  --text-secondary: #1a1a1a;
  --text-muted: #333333;
  --border-color: #000000;
  --border-color-strong: #000000;
}

[data-theme='dark'][data-high-contrast='true'] {
  --text-primary: #ffffff;
  --text-secondary: #f0f0f0;
  --text-muted: #e0e0e0;
  --border-color: #ffffff;
  --border-color-strong: #ffffff;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-size: var(--font-size-base);
  color: var(--text-primary);
  background-color: var(--bg-page);
  line-height: 1.5;
}

/* Layout */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1rem;
}

.sidebar-layout {
  display: grid;
  grid-template-columns: 250px 1fr;
  min-height: 100vh;
  gap: 0;
}

/* FIX-DEMO-BANNER-TOPBAR-OVERLAP-01 follow-up (sidebar regression,
   2026-09-25): the ONE shared offset for every element pinned below the
   demo/impersonation banner strip — .top-bar (below) and .sidebar (next
   rule) both read this SAME variable, so there is exactly one calc() to
   reason about, never two that could drift apart. Declared on :root (below
   the two source vars in the cascade, at the html/root level) rather than
   duplicated inline in each consumer.
   Bug this fixes: .sidebar was `position: sticky; top: 0` — unrelated to
   the banner height — so at rest it happened to sit flush (its normal-flow
   position already starts after the real, in-DOM banner), but once
   scrolled past the banner's height it snapped to literal y=0, sliding its
   top edge (logo, BETA pill) UNDER the still-sticky banner. MEASURED: demo
   banner 37px tall, sidebar.top went from 37px (at rest, correct) to 0px
   (scrolled 600px, wrong) while .top-bar (already using this pattern)
   stayed correctly at 37px both times. */
:root {
  --pinned-header-offset: calc(var(--site-banner-height, 0px) + var(--demo-banner-height, 0px) + var(--impersonation-banner-height, 0px));
}

.sidebar {
  background-color: var(--sidebar-bg);
  color: var(--sidebar-text);
  padding: 1.5rem 0;
  position: sticky;
  top: var(--pinned-header-offset);
  /* Shrunk by the same offset that pushes the sidebar's top down, so its
     BOTTOM never runs past the viewport (it used to: height:100vh + a
     positive top offset pushed the last nav item(s) off-screen with no way
     to reach them). overflow-y:auto (already set) makes the sidebar
     scroll internally if its content is still taller than what's left. */
  height: calc(100vh - var(--pinned-header-offset));
  /* FB-20261009-MOBILE-MENU-SETTINGS-UNREACHABLE-01: 100vh is the LARGE
     viewport on a mobile browser (toolbar hidden), so the bottom item sat
     under the toolbar. dvh tracks the visible viewport; browsers without it
     keep the line above. Containment stops scroll chaining to the page. */
  height: calc(100dvh - var(--pinned-header-offset));
  overscroll-behavior: contain;
  overflow-y: auto;
}

.sidebar-brand {
  padding: 0 1.5rem 1.5rem;
  border-bottom: 1px solid var(--sidebar-border);
  margin-bottom: 1rem;
}

.sidebar-brand h1 {
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: -0.5px;
}

.sidebar-brand .subtitle {
  font-size: 0.75rem;
  color: rgba(255, 255, 255, 0.6);
  margin-top: 0.25rem;
}

/* Beta status indicator, shown next to the product name. On authenticated
   pages it lives in .sidebar-brand — static markup there, unlike
   .top-bar-right, is NEVER rewritten by Nav.initPage() (renderNavigation only
   touches .sidebar-nav; updateUserInfo only touches .top-bar-right), so it
   needs no post-init JS insertion — see feedback_dashboard_topbar_rebuilt_by_nav.
   Also used on the public landing nav and signup header. Reuses
   --bg-info-soft/--text-info-strong (same tokens as .trial-chip), which
   already carry dark-mode overrides, so no separate [data-theme="dark"]
   block is needed here. */
.beta-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  white-space: nowrap;
  vertical-align: middle;
  background-color: var(--bg-info-soft);
  color: var(--text-info-strong);
}

.sidebar-nav {
  list-style: none;
}

.sidebar-nav li {
  margin: 0;
}

.sidebar-nav a {
  display: block;
  padding: 0.75rem 1.5rem;
  color: var(--sidebar-text);
  text-decoration: none;
  transition: var(--transition);
  border-left: 3px solid transparent;
}

.sidebar-nav a:hover {
  background-color: rgba(255, 255, 255, 0.1);
  color: var(--sidebar-text-hover);
}

.sidebar-nav a.active {
  background-color: var(--primary-color);
  color: white;
  border-left-color: var(--secondary-color);
}

/* FIX-DEMO-BANNER-TOPBAR-OVERLAP-01: no `overflow-y: auto` here — see the
   .top-bar comment below for why that property, not just the padding/margin
   pairing, was the other half of the bug. */
/* WEB-MOBILE-DASHBOARD-OVERFLOW-01: `.main-content` is the `1fr` track of
   `.sidebar-layout`'s grid (both at the >=769px `250px 1fr` and the
   <=768px single-column layout below). A grid item's default `min-width`
   is `auto`, not `0` — so it will not shrink narrower than its content's
   min-content width. A wide-but-unwrapped child (here: the async-loaded
   "Upcoming Events" table, whose `.status-badge` cells are
   `white-space: nowrap`) sets that min-content width, and the grid track
   grows to fit it instead of leaving `.table-responsive`'s own
   `overflow-x: auto` to scroll internally. MEASURED (headless Chrome,
   390x844, manager dashboard, after the table's async populate): document
   scrollWidth 479px > 390px viewport, with the SCHEDULED badge clipped at
   the right edge. `min-width: 0` lets the grid item shrink to its track's
   actual width, so the table scrolls sideways INSIDE its card instead of
   widening the whole page. */
.main-content {
  /* No top padding — see the .top-bar comment below for why. */
  padding: 0 2rem 2rem;
  min-width: 0;
}

/* FIX-DEMO-BANNER-TOPBAR-OVERLAP-01: two compounding bugs, both MEASURED
   with a headless-Chrome getBoundingClientRect() probe (web/tools/
   test-topbar-does-not-overlap-content.js), fixed together:

   1. .top-bar used to cancel .main-content's TOP padding with a matching
      negative top margin (`margin: -2rem -2rem 2rem -2rem`) so it sat flush
      against the grid cell's edge. That collapsing trick silently breaks the
      instant .top-bar becomes `position: sticky` (the >=769px block below):
      Chromium does not fold a sticky element's negative top margin into its
      ancestor's padding the way it does for a static/relative box, leaving a
      persistent ~32px gap (measured: an otherwise-identical box moved from
      y=0 to y=32 purely by switching position:static -> sticky).
      Fix: .main-content has NO top padding and .top-bar has NO top margin,
      so there's nothing to cancel and nothing for sticky to get wrong —
      .top-bar is simply flush with .main-content's top edge in normal flow.
      Left/right stay negative (full-bleed width) — only the TOP axis was
      ever affected.

   2. `.main-content { overflow-y: auto }` (now removed) made .main-content a
      CSS scroll container, which per spec becomes the "nearest ancestor
      scroll container" .top-bar's `position: sticky` resolves its `top`
      offset against — NOT the document/viewport, despite the property never
      actually clipping anything (.main-content's grid row always sizes to
      content, so its own scrollbar never engages; the *document* is the
      thing that visibly scrolls). Since .main-content's box is already
      pushed down by the demo/impersonation banners' real height (they're
      normal-flow siblings before .sidebar-layout in <body>), resolving
      .top-bar's `top: calc(--demo-banner-height + --impersonation-banner-
      height)` against .main-content's own (already-shifted) scrollport
      ADDED that height a second time — MEASURED: with the property present,
      an unscrolled demo page put .top-bar at 2x the banner's height instead
      of 1x. Removing `overflow-y: auto` (never load-bearing — nothing
      constrains .main-content's height, so it never actually overflowed)
      makes .top-bar resolve `top` against the document, matching the banner
      height exactly, both unscrolled and mid-scroll. */
/* Founder feedback #05, widened by founder feedback 2026-09-25 ("I would
   expect the nav bar to... behave just like [the DEMO banner]. pinned to the
   top in browser..."): keep the top bar in view while the page scrolls, at
   EVERY width, not just >=769px — it used to stop at the phone breakpoint
   (competing with the hamburger toggle was the original reason), but the
   founder now wants Quick Actions reachable on a phone without scrolling
   too, same as desktop. The background is already solid (--bg-card, themed)
   so content scrolling underneath never shows through. nav.js only ever
   rewrites .top-bar-right's CONTENTS (avatar menu, trial/studio chips) — it
   never touches .top-bar itself — so sticky positioning here survives
   Nav.initPage().

   Stacking order (top to bottom, fixed): demo banner -> impersonation
   banner -> top bar. Both banners are ALSO position:sticky;top:0 (z-index
   910 and 900 respectively below) and are prepended to <body> (outside
   .main-content), resolving against the same scrolling context as this bar
   (the document — see the FIX-DEMO-BANNER-TOPBAR-OVERLAP-01 comment above
   .main-content for why that scrolling context matters and used to be
   wrong). Left alone, any of these sticky elements stuck at top:0 would
   paint over this bar's Logout/avatar controls once scrolled. Instead the
   top bar's sticky offset is the sum of both banners' real rendered
   heights, published as --demo-banner-height (Nav._syncDemoBannerHeightVar,
   nav.js) and --impersonation-banner-height
   (Components.Impersonation._syncBannerHeightVar, components.js) — each var
   is simply absent (defaulting to 0px here) when its banner isn't present,
   and each is re-measured live (including on resize, via ResizeObserver)
   since a banner's text can wrap onto a second line, so neither height is a
   fixed constant. Two independently-sticky elements whose offsets are kept
   equal by a live-measured shared variable is deliberately NOT the same
   thing as one wrapping sticky header element — a single wrapper would mean
   moving .top-bar out of .main-content (it would need to span above the
   sidebar too, since the banners already do), a materially larger, riskier
   change to the grid layout for no measurable difference: this pairing
   already produces bar.top === banner.bottom to the pixel, both at rest and
   mid-scroll (test-topbar-does-not-overlap-content.js), because the offset
   is always the banner's real measured height, never a guess. .sidebar
   (above) uses the SAME --pinned-header-offset variable rather than
   repeating this calc() a second time — see its comment for the regression
   that happens when a pinned element ISN'T kept in sync with this one.

   z-index scale, low to high (page chrome never outranks an overlay):
   top bar 50 < impersonation banner 900 < demo banner 910 <
   sidebar-overlay/sidebar 998/999 < dropdowns/.modal 1000 <
   .pin-gate 1100 < .toast 2000. Both sticky banners sit below every
   overlay so a full-screen .modal or .pin-gate always paints over them,
   even though the banners are DOM-first and visually "on top" of the
   page content underneath them. */
.top-bar {
  background-color: var(--bg-card);
  padding: 1rem 2rem;
  box-shadow: var(--shadow);
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 0 -2rem 2rem -2rem;
  position: sticky;
  top: var(--pinned-header-offset);
  z-index: 50;
}

.top-bar-left {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.top-bar-right {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  flex-shrink: 0;
}

/* Trial-days-left chip (dashboard top bar). Built on the shared tone tokens
   (--bg-info-soft / --bg-warning-soft) which already carry dark-mode overrides,
   so no separate [data-theme="dark"] block is needed. Amber (.trial-chip-warn)
   flips on at <=3 days left, matching the settings-subscription banner.
   Hidden by default; the manager dashboard adds .trial-chip-shown when the
   subscription is trialing (a class, not an inline style, so the mobile media
   query below can hide it — an inline style would out-specify the query). */
.trial-chip {
  display: none;
  align-items: center;
  padding: 0.3rem 0.75rem;
  border-radius: 999px;
  border: 1px solid transparent;
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
  text-decoration: none;
  background-color: var(--bg-info-soft);
  color: var(--text-info-strong);
  transition: filter 0.15s ease;
}

.trial-chip.trial-chip-shown {
  display: inline-flex;
}

.trial-chip:hover {
  filter: brightness(0.97);
}

.trial-chip-warn {
  background-color: var(--bg-warning-soft);
  color: var(--text-warning-strong);
  border-color: var(--border-warning);
}

/* Current-studio-name chip (dashboard top bar). FB-20260624-03 — the
   multi-studio account→user model means someone can belong to several
   studios with nothing on the dashboard saying which one is active. Reuses
   --text-muted / --border-color (already dark-mode-token-backed, no new
   [data-theme="dark"] block needed). Same shown/hidden + mobile-hide pattern
   as .trial-chip: hidden by default, .studio-chip-shown reveals it, the
   375px media query below re-hides it so the no-wrap top bar can't overflow. */
.studio-chip {
  display: none;
  align-items: center;
  padding: 0.3rem 0.75rem;
  border-radius: 999px;
  border: 1px solid var(--border-color);
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--text-muted);
}

.studio-chip.studio-chip-shown {
  display: inline-flex;
}

.user-info {
  text-align: right;
}

.user-name {
  font-weight: 600;
  color: var(--text-primary);
}

.user-role {
  font-size: var(--font-size-sm);
  color: var(--text-muted);
}

/* Avatar User Menu */
.avatar-menu-container {
  position: relative;
}

.avatar-btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border-color);
  border-radius: 50px;
  background: var(--bg-card);
  cursor: pointer;
  transition: var(--transition);
}

.avatar-btn:hover {
  border-color: var(--primary-color);
  box-shadow: 0 0 0 2px var(--primary-light);
}

.avatar-circle {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--primary-color);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 0.85rem;
}

/* Larger size variant for card-style layouts (e.g. guardian dashboard's My
   Children card, UI-AVATAR-GUARDIAN-MYCHILDREN-01). Colors are inherited
   from .avatar-circle's theme-aware vars, so no dark-mode override needed. */
.avatar-circle-lg {
  width: 48px;
  height: 48px;
  font-size: 1.25rem;
}

.avatar-name {
  font-weight: 600;
  font-size: var(--font-size-sm);
  color: var(--text-primary);
}

.avatar-caret {
  font-size: 0.6rem;
  color: var(--text-muted);
  transition: transform 0.2s ease;
}

.avatar-menu-container.open .avatar-caret {
  transform: rotate(180deg);
}

.avatar-dropdown {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  min-width: 220px;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-lg);
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition:
    opacity 0.15s ease,
    transform 0.15s ease,
    visibility 0.15s;
}

.avatar-menu-container.open .avatar-dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.avatar-dropdown-header {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border-color);
}

.avatar-dropdown-header .user-name {
  font-weight: 600;
  font-size: var(--font-size-sm);
}

.avatar-dropdown-header .user-role {
  font-size: var(--font-size-xs);
  color: var(--text-muted);
  text-transform: capitalize;
}

.avatar-dropdown-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.6rem 1rem;
  border: none;
  background: none;
  font-size: var(--font-size-sm);
  color: var(--text-primary);
  cursor: pointer;
  text-align: left;
  transition: background 0.1s ease;
}

.avatar-dropdown-item:hover {
  background: var(--bg-hover);
}

.avatar-dropdown-item .item-icon {
  width: 1.25rem;
  text-align: center;
  flex-shrink: 0;
}

.avatar-dropdown-divider {
  height: 1px;
  background: var(--border-color);
  margin: 0.25rem 0;
}

.studio-switcher {
  padding: 0.25rem 0;
}

.studio-current {
  padding: 0.4rem 1rem;
  font-size: var(--font-size-xs);
  color: var(--text-muted);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

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

.avatar-dropdown-item.danger:hover {
  background: var(--bg-danger-soft);
}

/* In-app notification bell + panel (C7). Sits beside the avatar menu inside
   .top-bar-right, mounted by Components.Notifications.init() (components.js)
   from Nav.updateUserInfo() (nav.js) on every page load. Uses only themed
   var() tokens, same as the avatar dropdown above it, so no separate
   [data-theme="dark"] block is needed. */
.notif-bell-wrap {
  position: relative;
}

.notif-bell-btn {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--border-color);
  border-radius: 50%;
  background: var(--bg-card);
  cursor: pointer;
  font-size: 1.1rem;
  transition: var(--transition);
}

.notif-bell-btn:hover {
  border-color: var(--primary-color);
  box-shadow: 0 0 0 2px var(--primary-light);
}

.notif-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--danger-color);
  color: white;
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

.notif-panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  width: 340px;
  max-width: calc(100vw - 2rem);
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-lg);
  z-index: 1000;
  overflow: hidden;
}

.notif-panel[hidden] {
  display: none;
}

.notif-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border-color);
  font-weight: 600;
}

.notif-mark-all-btn {
  background: none;
  border: none;
  color: var(--primary-color);
  font-size: var(--font-size-sm);
  font-weight: 600;
  cursor: pointer;
  padding: 0;
}

.notif-panel-body {
  overflow-y: auto;
}

.notif-state {
  padding: 1.5rem 1rem;
  text-align: center;
  color: var(--text-muted);
  font-size: var(--font-size-sm);
}

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

.notif-item {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.65rem 1rem;
  border-bottom: 1px solid var(--border-color);
}

.notif-item:last-child {
  border-bottom: none;
}

.notif-item-unread {
  background: var(--bg-info-soft);
}

.notif-item-main {
  min-width: 0;
}

.notif-item-title {
  font-weight: 600;
  font-size: var(--font-size-sm);
  color: var(--text-primary);
}

.notif-item-unread .notif-item-title::before {
  content: '';
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 0.4rem;
  border-radius: 50%;
  background: var(--primary-color);
}

.notif-item-body {
  font-size: var(--font-size-sm);
  color: var(--text-secondary);
  margin-top: 0.15rem;
  word-break: break-word;
}

.notif-item-time {
  font-size: var(--font-size-xs);
  color: var(--text-muted);
  margin-top: 0.25rem;
}

.notif-item-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.25rem;
  flex-shrink: 0;
}

.notif-item-actions .btn-link {
  background: none;
  border: none;
  color: var(--primary-color);
  font-size: var(--font-size-xs);
  font-weight: 600;
  cursor: pointer;
  padding: 0;
  white-space: nowrap;
}

/* Phone width (375px target): the panel can't hang 340px off the right edge
   of a 375px viewport without overflowing, so it goes near-full-width and
   the position anchor moves to the bell wrap's left edge instead of right. */
@media (max-width: 480px) {
  .notif-panel {
    width: calc(100vw - 2rem);
    right: -0.5rem;
  }
}

/* Typography */
h1 {
  font-size: var(--font-size-3xl);
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 0.5rem;
}

h2 {
  font-size: var(--font-size-2xl);
  font-weight: 700;
  color: var(--text-primary);
  margin: 1.5rem 0 1rem 0;
}

h3 {
  font-size: var(--font-size-xl);
  font-weight: 600;
  color: var(--text-secondary);
  margin: 1rem 0 0.5rem 0;
}

p {
  margin-bottom: 1rem;
  color: var(--text-secondary);
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
}

.page-title {
  margin-bottom: 0.25rem;
}

.page-subtitle {
  color: var(--text-muted);
  font-size: var(--font-size-base);
}

/* Quick Actions "+" menu (founder feedback #11; relocated into the top bar
   by FIX-DEMO-BANNER-TOPBAR-OVERLAP-01, founder: "quickactions should be
   part of the nav bar always at the top of the view"). Lives in
   .top-bar-left, immediately after the page title, mounted by
   Components.mountQuickActionsInTopBar (components.js) — see that function
   for why it must run AFTER Nav.initPage(). Same open/outside-click/Escape
   pattern as the avatar menu (.avatar-menu-container in nav.js) — see
   Components.initQuickActionsMenu in components.js. */
.quick-actions-menu {
  position: relative;
  display: inline-block;
  flex-shrink: 0;
}

.quick-actions-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  white-space: nowrap;
}

/* UI-QUICK-ACTIONS-PLUS-EVERYWHERE-01 (founder, 2026-09-26): icon-only "+"
   at EVERY width now, not just below 768px — the founder liked the compact
   mobile trigger and asked for it everywhere. The `title`/`aria-label` on
   the button (components.js, both "Quick Actions") carry the hover text and
   accessible name now that the visible label text is gone unconditionally. */
.quick-actions-btn .quick-actions-label {
  display: none;
}

.quick-actions-dropdown {
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 0;
  min-width: 200px;
  max-width: min(240px, calc(100vw - 32px));
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-lg);
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition:
    opacity 0.15s ease,
    transform 0.15s ease,
    visibility 0.15s;
  padding: 0.25rem 0;
}

/* The trigger lives near the LEFT edge of the top bar (.top-bar-left, right
   after the page title) at every width, so the dropdown stays left-anchored
   (`left: 0` above) and extends rightward, always on-screen — there is no
   desktop/mobile split here unlike a right-edge-anchored control. */

.quick-actions-menu.open .quick-actions-dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.quick-actions-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.6rem 1rem;
  border: none;
  background: none;
  font-size: var(--font-size-sm);
  color: var(--text-primary);
  cursor: pointer;
  text-align: left;
  transition: background 0.1s ease;
}

.quick-actions-item:hover,
.quick-actions-item:focus-visible {
  background: var(--bg-hover);
}

/* Cards */
.card {
  background-color: var(--bg-card);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow);
  padding: 1.5rem;
  margin-bottom: 1.5rem;
}

.card-header {
  border-bottom: 1px solid var(--border-color);
  padding-bottom: 1rem;
  margin-bottom: 1rem;
}

.card-header h3 {
  margin: 0;
}

.card-body {
  flex: 1;
}

.card-footer {
  border-top: 1px solid var(--border-color);
  padding-top: 1rem;
  margin-top: 1rem;
}

/* Grid Layouts */
.grid {
  display: grid;
  gap: 1.5rem;
}

.grid-2 {
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}

.grid-3 {
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}

.grid-4 {
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

/* DASH-FEEDBACK-POLISH-01 #19a: with the money metric cards removed, a
   .grid-4 that's down to its one remaining card (Active Students) would
   otherwise stretch that card to fill the whole row via auto-fit's 1fr
   track. Cap it so a lone metric card reads as a card, not a banner. */
.grid-4 .metric-card:only-child {
  max-width: 300px;
}

/* ============================================
   Metrics Cards - Theme-Aware Design
   ============================================ */
.metric-card {
  background: var(--bg-card);
  border-radius: var(--border-radius);
  padding: 1.5rem;
  box-shadow: var(--shadow);
  text-align: center;
  border: 1px solid var(--border-color);
  transition: var(--transition);
  container-type: inline-size;
}

.metric-card:hover {
  box-shadow: var(--shadow-lg);
}

.metric-card a.metric-card-link {
  display: block;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}

.metric-card:has(a.metric-card-link):hover {
  transform: translateY(-2px);
}

.metric-card .metric-label {
  color: var(--text-secondary);
  font-size: var(--font-size-sm);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  font-weight: 600;
  margin-bottom: 0.5rem;
}

.metric-card .metric-value {
  font-size: clamp(1.1rem, 4.5vw, var(--font-size-3xl));
  font-size: clamp(1.1rem, 14cqi, var(--font-size-3xl));
  font-weight: 700;
  color: var(--text-primary);
  margin: 0.5rem 0;
  line-height: 1.2;
}

.metric-card .metric-change {
  font-size: var(--font-size-sm);
  margin-top: 0.5rem;
  color: var(--text-muted);
}

.metric-change.positive {
  color: var(--secondary-color);
}

.metric-change.negative {
  color: var(--danger-color);
}

.metric-change.neutral {
  color: var(--text-muted);
}

.metric-card .metric-note {
  font-size: var(--font-size-sm);
  margin-top: 0.25rem;
  color: var(--text-muted);
}

/* ---- Metric Card Variants ---- */

/* Primary variant - blue accent */
.metric-card.metric-primary {
  border-left: 4px solid var(--primary-color);
}

.metric-card.metric-primary .metric-value {
  color: var(--primary-color);
}

/* Success variant - green accent (for positive metrics like revenue, paid) */
.metric-card.metric-success {
  border-left: 4px solid var(--secondary-color);
}

.metric-card.metric-success .metric-value {
  color: var(--secondary-color);
}

/* Warning variant - orange accent (for attention items) */
.metric-card.metric-warning {
  border-left: 4px solid var(--warning-color);
}

.metric-card.metric-warning .metric-value {
  color: var(--warning-color);
}

/* Danger variant - red accent (for overdue, outstanding) */
.metric-card.metric-danger {
  border-left: 4px solid var(--danger-color);
}

.metric-card.metric-danger .metric-value {
  color: var(--danger-color);
}

/* Neutral variant - gray accent (for counts, statistics) */
.metric-card.metric-neutral {
  border-left: 4px solid var(--gray-400);
}

.metric-card.metric-neutral .metric-value {
  color: var(--text-primary);
}

/* ---- Dark Mode Adjustments ---- */
[data-theme='dark'] .metric-card {
  border-color: var(--border-color);
}

[data-theme='dark'] .metric-card.metric-success .metric-value {
  color: #34d399; /* Brighter green for dark mode */
}

[data-theme='dark'] .metric-card.metric-warning .metric-value {
  color: #fbbf24; /* Brighter amber for dark mode */
}

[data-theme='dark'] .metric-card.metric-danger .metric-value {
  color: #f87171; /* Brighter red for dark mode */
}

/* ---- High Contrast Mode ---- */
[data-high-contrast='true'] .metric-card {
  border-width: 2px;
}

[data-high-contrast='true'] .metric-card .metric-value {
  color: var(--text-primary);
}

[data-high-contrast='true'] .metric-card.metric-primary,
[data-high-contrast='true'] .metric-card.metric-success,
[data-high-contrast='true'] .metric-card.metric-warning,
[data-high-contrast='true'] .metric-card.metric-danger {
  border-left-width: 6px;
}

/* ---- Large Font Mode ---- */
[data-font-size='large'] .metric-card {
  padding: 2rem;
}

[data-font-size='large'] .metric-card .metric-value {
  font-size: clamp(1.3rem, 5.5vw, 2.5rem);
  font-size: clamp(1.3rem, 16cqi, 2.5rem);
}

/* Tables */
.table-responsive {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1rem;
}

table thead {
  background-color: var(--bg-table-header);
}

table th {
  padding: 1rem;
  text-align: left;
  font-weight: 600;
  color: var(--text-secondary);
  border-bottom: 2px solid var(--border-color);
}

table td {
  padding: 1rem;
  border-bottom: 1px solid var(--border-color);
  color: var(--text-primary);
}

table tbody tr {
  transition: var(--transition);
}

table tbody tr:hover {
  background-color: var(--bg-hover);
}

/* FB-20261001-TABLE-UX-01: row name/avatar as the row opener (flag
   feature.table_ux). A real <button>/<a> so it's keyboard reachable and
   gets a visible focus ring; styled to read as a name, not a button. */
.row-open-link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  font-weight: 600;
  color: var(--text-primary);
  cursor: pointer;
  text-align: left;
  text-decoration: none;
}

.row-open-link:hover,
.row-open-link:focus-visible {
  color: var(--primary-color);
  text-decoration: underline;
}

.row-open-link:focus-visible {
  outline: 2px solid var(--primary-color);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Avatar + name (or any short cluster) inside a stacked cell. The flex
   lives on the span so the td can stay a table-cell on wide screens. */
.cell-inline {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

/* List empty and loading. Components.loadingRow / emptyRow / emptyBlock.
   Colors are tokens so dark mode follows the theme. */
.empty-state {
  padding: 1.25rem 0.5rem;
}

.empty-state__title {
  margin: 0 0 0.25rem;
  font-weight: 600;
  color: var(--text-primary);
}

.empty-state__message {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}

.empty-state .btn {
  margin-top: 0.75rem;
}

.skeleton-line {
  height: 0.75rem;
  width: min(16rem, 70%);
  margin: 0.85rem auto;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--bg-hover) 25%, var(--border-color) 50%, var(--bg-hover) 75%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.2s ease-in-out infinite;
}

@keyframes skeleton-shimmer {
  0% {
    background-position: 100% 0;
  }
  100% {
    background-position: -100% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton-line {
    animation: none;
  }
}

/* FB-20261001-TABLE-UX-01: TableFilters bar (web/js/table-filters.js, flag
   feature.table_ux). Placed directly above the table it filters; the
   flex-wrap layout is what gives it the "compact bar" mobile behavior for
   free — no separate narrow-viewport markup needed. All colors are themed
   custom properties, so no [data-theme="dark"] override is needed. */
.table-filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  background-color: var(--bg-card-alt);
  border: 1px solid var(--border-color);
  border-bottom: none;
  border-radius: 6px 6px 0 0;
}

.table-filter-field {
  flex: 1 1 140px;
  min-width: 110px;
}

.table-filter-input {
  width: 100%;
  padding: 0.4rem 0.6rem;
  font-size: 0.85rem;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  background-color: var(--bg-card);
  color: var(--text-primary);
}

/* FB-20260922-MOBILE-LAYOUT-01 #2: opt-in modifier (`.table-responsive--stack`
   on the wrapper, students.html today) that turns a data table into stacked
   cards below 640px, instead of the default `.table-responsive` behavior of
   letting the table overflow and scroll sideways inside its box — which is
   how the Actions column ended up off-screen, reachable only by a sideways
   scroll a booth visitor is unlikely to find. Every column becomes a full-
   width row with a caption (from `data-label` on each <td>), so nothing
   sits off-screen and there's nothing to scroll. Desktop (>=640px) and
   every OTHER `.table-responsive` are unaffected — this is scoped to the
   modifier class, not the base class.
   Cells keep their table roles explicitly (role="table"/"rowgroup"/"row"/
   "cell" on the markup in students.html) because overriding `display` off
   `table`/`table-row`/`table-cell` drops the implicit ARIA table semantics
   in Chrome/Firefox. */
@media (max-width: 640px) {
  .table-responsive--stack {
    overflow-x: visible;
  }

  .table-responsive--stack table {
    display: block;
    width: 100%;
  }

  /* Visually hidden, not display:none — screen readers still get the
     column names via the thead, same "clip" technique as .sr-only. */
  .table-responsive--stack thead {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .table-responsive--stack tbody,
  .table-responsive--stack tr,
  .table-responsive--stack td {
    display: block;
    width: 100%;
  }

  .table-responsive--stack tr {
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius);
    margin-bottom: 0.75rem;
    padding: 0 0.75rem;
  }

  .table-responsive--stack td {
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--border-color);
  }

  .table-responsive--stack td:last-child {
    border-bottom: none;
  }

  /* Per-field cells (carry data-label) render as a "Label ... value" row.
     The loading/empty colspan message rows have no data-label and fall
     through to the plain block styling above, keeping their existing
     centered text untouched. */
  .table-responsive--stack td[data-label] {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    text-align: right;
  }

  .table-responsive--stack td[data-label]::before {
    content: attr(data-label);
    flex-shrink: 0;
    text-align: left;
    font-weight: 600;
    color: var(--text-secondary);
  }

  /* Actions: right-align the button(s) flush with the card edge instead of
     against a redundant "Actions" caption. flex-wrap: some rows (e.g.
     schedule.html's Upcoming Sessions, UI-UPCOMING-SESSIONS-MOBILE-01) carry
     up to three buttons — without wrap they'd overflow the 390px card width
     instead of stacking, reintroducing the sideways-scroll problem this
     modifier exists to remove. */
  .table-responsive--stack td.cell-actions {
    justify-content: flex-end;
    flex-wrap: wrap;
  }

  .table-responsive--stack td.cell-actions::before {
    display: none;
  }

  /* UI-UPCOMING-SESSIONS-MOBILE-01: a value made of several inline parts
     (e.g. schedule.html's "Student / Plan" and "Date & Time" cells) is
     wrapped by its template in a single `.cell-stack` span so this rule can
     turn it into its own column, independent of the label/value split above
     — each part gets its own line instead of wrapping mid-word inside a
     cramped shared line. `.cell-sep`/`.cell-br` are the desktop-only
     separators (" -- ", a line break) between parts that a stacked layout
     no longer needs. */
  .table-responsive--stack .cell-stack {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.15rem;
  }

  .table-responsive--stack .cell-sep,
  .table-responsive--stack .cell-br {
    display: none;
  }
}

table .status-badge {
  display: inline-block;
  padding: 0.25rem 0.75rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
}

.status-active {
  background-color: var(--bg-success-soft);
  color: var(--text-success-strong);
}

.status-pending {
  background-color: var(--bg-warning-soft);
  color: var(--text-warning-strong);
}

.status-completed {
  background-color: var(--bg-info-soft);
  color: var(--text-info-strong);
}

.status-cancelled {
  background-color: var(--bg-danger-soft);
  color: var(--text-danger-strong);
}

.status-paid {
  background-color: var(--bg-success-soft);
  color: var(--text-success-strong);
}

.status-outstanding {
  background-color: var(--bg-danger-soft);
  color: var(--text-danger-strong);
}

.status-overdue {
  background-color: var(--bg-danger-soft);
  color: var(--text-danger-strong);
}

.status-inactive {
  background-color: var(--bg-neutral-soft);
  color: var(--text-neutral-strong);
}

.status-error {
  background-color: var(--bg-danger-soft);
  color: var(--text-danger-strong);
}

/* SCHED-PAST-LESSONS-UNDISCOVERABLE-01 batch, dark-mode badge fix:
   schedule.html's session-status badges ('scheduled', 'rescheduled') used to
   fall through Utils.getStatusBadgeClass's default bucket (.status-pending,
   amber/yellow), which reads as low-contrast yellow text on a grey card in
   dark mode. These reuse existing AA-contrast token pairs -- the same
   --bg-*-soft/--text-*-strong design already used by .status-active etc.
   above, no hardcoded hex -- and match the colors schedule.html already
   uses for these two statuses elsewhere (its .status-dot.scheduled /
   .status-dot.rescheduled, and the calendar legend). */
.status-session-scheduled {
  background-color: var(--bg-success-soft);
  color: var(--text-success-strong);
}

.status-session-rescheduled {
  background-color: var(--group-accent-soft);
  color: var(--group-accent-strong);
}

/* Pagination */
.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.pagination-current {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  padding: 0.25rem 0.5rem;
  font-weight: 700;
  color: var(--primary-color);
  background-color: var(--bg-card-alt, #f0f0f0);
  border-radius: var(--border-radius);
  font-size: 0.85rem;
}

/* Buttons */
button,
.btn {
  display: inline-block;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: var(--border-radius);
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: var(--transition);
  text-decoration: none;
}

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

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

.btn-secondary {
  background-color: var(--bg-card-alt);
  color: var(--text-primary);
  border: 1px solid var(--border-color);
}

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

.btn-danger {
  background-color: var(--danger-color);
  color: white;
}

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

.btn-success {
  background-color: var(--secondary-color);
  color: white;
}

.btn-success:hover {
  background-color: var(--secondary-color-hover);
}

.btn-sm {
  padding: 0.25rem 0.75rem;
  font-size: 0.875rem;
}

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

.btn-group {
  display: flex;
  gap: 0.5rem;
}

/* Forms */
.form-group {
  margin-bottom: 1.5rem;
}

label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
  color: var(--text-secondary);
}

input,
select,
textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid var(--border-color-strong);
  border-radius: var(--border-radius);
  font-size: var(--font-size-base);
  font-family: inherit;
  transition: var(--transition);
  background-color: var(--bg-input);
  color: var(--text-primary);
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--primary-color);
  box-shadow: 0 0 0 3px var(--primary-light);
}

input[type='checkbox'],
input[type='radio'] {
  width: auto;
  margin-right: 0.5rem;
}

.form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
}

.consent-group {
  margin-top: 0.75rem;
  padding: 0.75rem;
  background: var(--bg-card-alt);
  border: 1px solid var(--border-color);
  border-radius: 6px;
}

.consent-group label {
  display: flex;
  gap: 0.5rem;
  align-items: flex-start;
  font-weight: 400;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--text-muted);
  cursor: pointer;
  margin: 0;
}

.consent-group input[type='checkbox'] {
  margin-top: 0.2rem;
  margin-right: 0;
  flex-shrink: 0;
}

.consent-group a {
  color: var(--link-color);
  text-decoration: underline;
}

.consent-note {
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-top: 0.4rem;
  padding-left: 1.5rem;
}

/* Alerts */
.alert {
  padding: 1rem;
  border-radius: var(--border-radius);
  margin-bottom: 1rem;
}

.alert-info {
  background-color: var(--bg-info-soft);
  border-left: 4px solid var(--primary-color);
  color: var(--text-info-strong);
}

.alert-success {
  background-color: var(--bg-success-soft);
  border-left: 4px solid var(--secondary-color);
  color: var(--text-success-strong);
}

.alert-warning {
  background-color: var(--bg-warning-soft);
  border-left: 4px solid var(--warning-color);
  color: var(--text-warning-strong);
}

.alert-danger {
  background-color: var(--bg-danger-soft);
  border-left: 4px solid var(--danger-color);
  color: var(--text-danger-strong);
}

/* Inline soft warning panel (e.g. reschedule proposed-time fields,
   inline closure banners). Pairs background + border + text via
   semantic vars so dark mode flips automatically. */
.proposed-slot-fields {
  padding: 0.75rem;
  background: var(--bg-warning-soft);
  border: 1px solid var(--border-warning);
  border-radius: var(--border-radius);
  margin-bottom: 1rem;
  color: var(--text-warning-strong);
}

/* List View */
.list-item {
  display: flex;
  align-items: center;
  padding: 1rem;
  border-bottom: 1px solid var(--border-color);
  transition: var(--transition);
}

.list-item:last-child {
  border-bottom: none;
}

.list-item:hover {
  background-color: var(--bg-hover);
}

.list-item-content {
  flex: 1;
}

.list-item-title {
  font-weight: 600;
  color: var(--text-primary);
}

.list-item-subtitle {
  font-size: var(--font-size-sm);
  color: var(--text-muted);
}

.list-item-meta {
  text-align: right;
  color: var(--text-muted);
  font-size: var(--font-size-sm);
}

/* Clickable dashboard rows/list-items that navigate to the entity's view/edit
   page (founder feedback #08 + #10). .list-item already has a hover
   background (above); this class adds the pointer cursor plus a
   focus-visible ring so a keyboard user (Tab, then Enter/Space) can see and
   activate it — real interactive elements (an inner button/link) still work
   normally since the handlers live on the row/item itself. */
tr.row-link,
.list-item-link {
  cursor: pointer;
}

tr.row-link:hover {
  background-color: var(--bg-hover);
}

tr.row-link:focus-visible,
.list-item-link:focus-visible {
  outline: 2px solid var(--primary-color);
  outline-offset: -2px;
}

/* Calendar (Simple) */
.calendar {
  background: var(--bg-card);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow);
  padding: 1.5rem;
}

.calendar-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1.5rem;
}

.calendar-weekdays {
  display: grid;
  /* WEB-MOBILE-SCHEDULE-MONTH-OVERFLOW-01: bare `1fr` is `minmax(auto, 1fr)`
     — a grid item's "automatic minimum size" only collapses to 0 when the
     track's own minimum is the `auto` keyword, so each column refuses to
     shrink below ITS cell's min-content. `minmax(0, 1fr)` removes that
     floor; the weekday labels are short enough that nothing visibly
     changes until the row is actually squeezed. */
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.calendar-weekday {
  text-align: center;
  font-weight: 600;
  color: var(--text-muted);
  font-size: var(--font-size-sm);
  text-transform: uppercase;
  padding: 0.5rem 0;
}

.calendar-days {
  display: grid;
  /* WEB-MOBILE-SCHEDULE-MONTH-OVERFLOW-01: see .calendar-weekdays above.
     This is the actual forcing child in the reported bug — MEASURED
     (headless Chrome, 375px): with bare `1fr` columns, `.calendar-days`'
     own min-content was 275px (each of the 7 `.calendar-day` cells
     refusing to shrink below its day-number's content width), which made
     `#calendar` (padding included) 323px and the Calendar card 371px —
     wider than its 343px `.grid-2` container, forcing the whole page to
     scroll sideways. `minmax(0, 1fr)` lets the cells shrink to fit; a
     1-2 digit day number still fits comfortably at any phone width. */
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.5rem;
}

.calendar-day {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  cursor: pointer;
  transition: var(--transition);
  font-size: var(--font-size-sm);
  color: var(--text-primary);
  background-color: var(--bg-card);
}

.calendar-day:hover {
  background-color: var(--primary-light);
  border-color: var(--primary-color);
}

.calendar-day.today {
  font-weight: 700;
  color: var(--primary-color);
}

.calendar-day.other-month {
  color: var(--text-muted);
  opacity: 0.5;
}

/* Selected must come last to override other states */
.calendar-day.selected {
  background-color: var(--primary-light);
  border: 3px solid var(--primary-color);
  font-weight: 700;
  font-size: 1.1em;
}

/* Modal */
.modal {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: var(--modal-backdrop);
  z-index: 1000;
  align-items: center;
  justify-content: center;
}

.modal.active {
  display: flex;
}

.modal-content {
  background: var(--bg-card);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-lg);
  max-width: 600px;
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
}

.modal-header {
  padding: 1.5rem;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.modal-body {
  padding: 1.5rem;
}

.modal-footer {
  padding: 1.5rem;
  border-top: 1px solid var(--border-color);
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}

.modal-close {
  background: none;
  border: none;
  font-size: 1.5rem;
  color: var(--text-muted);
  cursor: pointer;
  padding: 0;
}

/* ============================================
   Policy-change notice (POLICY-CHANGE-NOTICE-WORDING.md, Nav.renderPolicyNotice)
   Deliberately NOT .modal — Components' global Escape-key/backdrop-click
   handler closes any .modal.active with no recorded action, which would let
   the notice silently disappear rather than being accepted or dismissed
   (doc §2: "a dismissal is a record, not a disappearance"). Same backdrop/
   card tokens as .modal so dark mode is inherited for free; no close (X)
   button, no Escape handler — the only way off is one of its two labelled
   buttons, each of which calls the accept/dismiss endpoint first.
   ============================================ */
.policy-notice-overlay {
  display: flex;
  position: fixed;
  inset: 0;
  background-color: var(--modal-backdrop);
  z-index: 1200;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

.policy-notice-card {
  background: var(--bg-card);
  color: var(--text-primary);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-lg);
  max-width: 32rem;
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  padding: 1.5rem;
}

.policy-notice-card h3 {
  margin-top: 0;
}

.policy-notice-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.75rem;
  margin-top: 1.5rem;
}

.policy-notice-studio-text {
  margin-top: 1rem;
  padding: 0.75rem;
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  background: var(--bg-page);
  white-space: pre-wrap;
}

.policy-notice-versions {
  color: var(--text-muted);
  font-size: 0.85rem;
}

@media (max-width: 480px) {
  .policy-notice-actions {
    flex-direction: column-reverse;
  }
  .policy-notice-actions button {
    width: 100%;
  }
}

/* ============================================
   Restricted-tabs PIN prompt (Components.RestrictedTabs)
   Deliberately NOT .modal: the generic Escape/backdrop handlers close any
   .modal.active without telling the prompt, which would strand its
   promise. Same backdrop, card and theme tokens, so dark mode is inherited.
   Mobile-first: the card is 90% wide up to 22rem, comfortable at 375px.
   ============================================ */
.pin-gate {
  display: none;
  position: fixed;
  inset: 0;
  /* OPAQUE, not the translucent --modal-backdrop a .modal uses.
     FB-20260924-PIN-GATE-HIDE-BACKGROUND-01 (founder screenshot,
     2026-09-24): a translucent gate let Analytics' rendered layout show
     through, every card reading $0 / 0 — no data leaked (the gated APIs
     answer 423 until unlock), but it looked like an empty studio, which
     matters in a demo. Belt-and-suspenders with the .rt-gate-hiding class
     below, which removes the page content from the render tree entirely;
     this covers the instant between the gate appearing and that class
     taking effect, and any future gated page that forgets to hide first. */
  background-color: var(--bg-page);
  z-index: 1100;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

.pin-gate.active {
  display: flex;
}

/* FB-20260924-PIN-GATE-HIDE-BACKGROUND-01: Components.RestrictedTabs adds
   this to <body> for a manager on every gated page (Analytics, Invoices,
   Conversations/Messaging) for as long as the PIN prompt might be needed —
   from the moment ensureUnlocked() starts checking status until either the
   check comes back "no prompt needed" or the PIN is entered correctly.
   visibility (not display:none) keeps the layout box so nothing reflows
   when it's removed. */
body.rt-gate-hiding .sidebar-layout {
  visibility: hidden;
}

.pin-gate-card {
  background: var(--bg-card);
  color: var(--text-primary);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-lg);
  width: 90%;
  max-width: 22rem;
  padding: 1.5rem;
  text-align: center;
}

.pin-gate-title {
  margin: 0 0 0.5rem;
  font-size: 1.25rem;
}

.pin-gate-desc {
  margin: 0 0 1rem;
  color: var(--text-muted);
  font-size: 0.9rem;
}

.pin-gate-input {
  display: block;
  width: 100%;
  max-width: 14rem;
  margin: 0 auto 0.75rem;
  padding: 0.6rem 0.75rem;
  font-size: 1.75rem;
  letter-spacing: 0.5em;
  text-align: center;
  background: var(--bg-input);
  color: var(--text-primary);
  border: 1px solid var(--border-color-strong);
  border-radius: var(--border-radius);
}

.pin-gate-input:focus {
  outline: 2px solid var(--primary-color);
  outline-offset: 1px;
}

.pin-gate-error {
  min-height: 1.25rem;
  margin-bottom: 0.75rem;
  color: var(--danger-color);
  font-size: 0.9rem;
}

.pin-gate-actions {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
}

/* ============================================
   Mobile Hamburger Menu
   ============================================ */
.hamburger-btn {
  display: none;
  background: none;
  border: none;
  color: var(--text-primary);
  font-size: 1.5rem;
  cursor: pointer;
  padding: 0.25rem;
  line-height: 1;
  border-radius: 4px;
  transition: var(--transition);
}

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

.sidebar-overlay {
  position: fixed;
  inset: 0;
  background: var(--modal-backdrop);
  z-index: 998;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
}

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

.sidebar-close-btn {
  display: none;
  background: none;
  border: none;
  color: var(--sidebar-text);
  font-size: 1.5rem;
  cursor: pointer;
  padding: 0.25rem 0.5rem;
  line-height: 1;
  border-radius: 4px;
  position: absolute;
  top: 1.25rem;
  right: 0.75rem;
}

.sidebar-close-btn:hover {
  color: var(--sidebar-text-hover);
  background-color: rgba(255, 255, 255, 0.1);
}

/* Responsive */
@media (max-width: 768px) {
  .sidebar-layout {
    grid-template-columns: 1fr;
  }

  /* The dashboard top bar is a no-wrap flex row down to 375px; the trial chip
     would push user-info + Logout into horizontal overflow. Hide it on narrow
     screens — the trial countdown stays available in Settings → Subscription.
     Overrides .trial-chip.trial-chip-shown (same specificity, declared later). */
  .trial-chip.trial-chip-shown {
    display: none;
  }

  /* Same overflow reasoning as the trial chip above. */
  .studio-chip.studio-chip-shown {
    display: none;
  }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    width: 280px;
    height: 100vh;
    /* FB-20261009-MOBILE-MENU-SETTINGS-UNREACHABLE-01: the large-viewport vh
       left Settings under the browser toolbar; dvh is the visible area.
       contain stops the end-of-scroll gesture chaining to the page behind. */
    height: 100dvh;
    overscroll-behavior: contain;
    z-index: 999;
    transform: translateX(-100%);
    transition: transform 0.3s ease;
  }

  .sidebar.open {
    transform: translateX(0);
  }

  .hamburger-btn {
    display: block;
  }

  .sidebar-close-btn {
    display: block;
  }

  .main-content {
    /* No top padding — mirrors the desktop rule above (.top-bar has no top
       margin to cancel it). Not sticky below 769px, so this never hit the
       negative-margin/sticky quirk, but kept symmetric for one rule to
       reason about. */
    padding: 0 1rem 1rem;
  }

  .top-bar {
    margin: 0 -1rem 1rem -1rem;
    padding: 0.75rem 1rem;
  }

  .page-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
  }

  /* WEB-MOBILE-DASHBOARD-OVERFLOW-01: schedule.html's view-toggle +
     Recurring + "+ Schedule Lesson" button row (`.page-header-actions`) is a
     no-wrap flex row wide enough on its own to overflow a 390px viewport by
     a few px once `.main-content` stopped absorbing it (previous fix, same
     bug ID, above) — MEASURED via a headless getBoundingClientRect scan:
     addSessionBtn's right edge sat 4px past the viewport. Wrapping it is
     enough at every width the buttons must appear (Recurring/+Schedule
     Lesson are teacher/manager-only and hidden by default via inline
     `style="display: none"` until role checks resolve). */
  .page-header-actions {
    flex-wrap: wrap;
    justify-content: flex-end;
  }

  /* FIX-DEMO-BANNER-TOPBAR-OVERLAP-01: Quick Actions lives in .top-bar-left
     (see the founder's mockup note above the base rules), not .page-header
     — the top bar is already a tight no-wrap row down to 375px (see the
     .top-bar comment near its sticky rule). The label text itself is now
     hidden unconditionally (`.quick-actions-btn .quick-actions-label` above,
     UI-QUICK-ACTIONS-PLUS-EVERYWHERE-01), so this block only still needs to
     keep the title truncating and the icon-only button's padding tight. */
  .top-bar-left:has(#quickActionsSlot) {
    min-width: 0;
    flex: 1 1 auto;
  }

  .top-bar-left:has(#quickActionsSlot) .quick-actions-btn {
    padding: 0.5rem 0.7rem;
  }

  /* Let the title truncate instead of pushing the button off-screen — only
     matters once something competes with it in .top-bar-left for the first
     time (Quick Actions, above). */
  .top-bar-left:has(#quickActionsSlot) h2 {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* WEB-MOBILE-SCHEDULE-MONTH-OVERFLOW-01: bare `1fr` here is
     `minmax(auto, 1fr)` — the track refuses to shrink narrower than the
     widest grid item's min-content (a grid item's default `min-width` is
     `auto`, not `0`; same defect class as WEB-MOBILE-DASHBOARD-OVERFLOW-01).
     `.grid-3`/`.grid-4` below do NOT share this: their `minmax(…px, 1fr)`
     already gives the track an explicit, non-`auto` minimum, so the
     content-based "automatic minimum size" override never applies to them
     (MEASURED: a grid-3/4 track's min size is just its declared px floor,
     not its content). `.grid-2` is the only one of the three using bare
     `1fr`, which is why only it needed this fix. */
  .grid-2 {
    grid-template-columns: minmax(0, 1fr);
  }

  .grid-3 {
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  }

  .grid-4 {
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  }

  table {
    font-size: 0.875rem;
  }

  table th,
  table td {
    padding: 0.75rem;
  }

  h1 {
    font-size: 1.5rem;
  }

  h2 {
    font-size: 1.25rem;
  }
}

/* FIX-DEMO-BANNER-TOPBAR-OVERLAP-01: founder, 2026-09-25 — "SMALL SCREENS
   (...): the bar collapses. Keep the page title and the compact + Quick
   Actions button visible ... move the right-hand group (studio chip, bell,
   avatar) into a single dropdown ... OR hide the studio chip text."
   The studio/trial chips are already hidden below 768px (above). Of the two
   remaining right-hand controls:
     - the notification bell is ALREADY icon-only (no label text, a 40px
       round button) — it isn't a meaningful width cost, so it stays visible
       rather than being folded away (avoids re-parenting its panel's
       positioning logic for no width benefit); a "Notifications" entry was
       ALSO added to the avatar dropdown (nav.js) so it's reachable from the
       single collapsed menu either way.
     - the avatar's NAME text is real width cost and adds nothing the
       dropdown itself doesn't already say (its header repeats the full
       name) — collapsed to icon+caret only. Opening it still reaches the
       SAME full menu (studio switcher, Notifications, Settings,
       Subscription, View As, Submit Feedback, Sign Out) — nothing lost. */
@media (max-width: 640px) {
  .avatar-name {
    display: none;
  }
}

/* Utilities */
.text-center {
  text-align: center;
}

.text-right {
  text-align: right;
}

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

.font-bold {
  font-weight: 700;
}

.mt-1 {
  margin-top: 0.5rem;
}

.mt-2 {
  margin-top: 1rem;
}

.mt-4 {
  margin-top: 2rem;
}

.mb-1 {
  margin-bottom: 0.5rem;
}

.mb-2 {
  margin-bottom: 1rem;
}

.mb-4 {
  margin-bottom: 2rem;
}

.p-1 {
  padding: 0.5rem;
}

.p-2 {
  padding: 1rem;
}

.gap-1 {
  gap: 0.5rem;
}

.gap-2 {
  gap: 1rem;
}

.hidden {
  display: none;
}

.flex {
  display: flex;
}

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

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

.loading {
  opacity: 0.6;
  pointer-events: none;
}

.spinner {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  border: 2px solid var(--gray-300);
  border-top-color: var(--primary-color);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

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

/* ===== Chart Styles ===== */
.chart-container {
  position: relative;
  height: 300px;
  margin: 1rem 0;
  padding: 1rem 0;
}

/* ===== List Item Styles ===== */
.list-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.list-group .list-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem;
  border-bottom: 1px solid var(--border-color);
  transition: background-color 0.2s;
}

.list-group .list-item:hover {
  background-color: var(--bg-hover);
}

.list-group .list-item:last-child {
  border-bottom: none;
}

.list-group .list-item-content {
  flex: 1;
}

.list-group .list-item-title {
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: 0.25rem;
}

.list-group .list-item-meta {
  color: var(--text-muted);
  font-size: var(--font-size-sm);
}

.list-item-value {
  font-weight: 600;
  color: var(--primary-color);
  white-space: nowrap;
  margin-left: 1rem;
}

/* ===== Statistics Grid ===== */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 1rem;
}

.stat-item {
  text-align: center;
  padding: 1rem;
  background-color: var(--bg-card-alt);
  border-radius: 0.5rem;
  border: 1px solid var(--border-color);
}

.stat-label {
  font-size: var(--font-size-sm);
  color: var(--text-muted);
  font-weight: 500;
  margin-bottom: 0.5rem;
}

.stat-value {
  font-size: var(--font-size-2xl);
  font-weight: 700;
  color: var(--primary-color);
}

/* ===== Table Responsive ===== */
.table-responsive {
  overflow-x: auto;
}

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

.data-table thead {
  background-color: var(--bg-table-header);
}

.data-table th {
  padding: 1rem;
  text-align: left;
  font-weight: 600;
  color: var(--text-secondary);
  border-bottom: 2px solid var(--border-color);
}

.data-table td {
  padding: 1rem;
  border-bottom: 1px solid var(--border-color);
  color: var(--text-primary);
}

.data-table tbody tr:hover {
  background-color: var(--bg-hover);
}

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

/* ============================================
   Room Utilization Heatmap (analytics.html, behind feature.room_utilization)
   ============================================ */
/* Scroll container — 24 hourly columns never fit at the 375px mobile-first
   target, so this scrolls horizontally instead of squeezing cells unreadably
   thin. The "Scroll horizontally..." hint text above it (analytics.html) is
   the visible scroll affordance. */
.room-util-heatmap-wrap {
  overflow-x: auto;
  padding-bottom: 0.5rem;
}

.room-util-heatmap-grid {
  display: grid;
  grid-template-columns: 4.5rem repeat(var(--room-util-heatmap-cols, 24), minmax(1.75rem, 1fr));
  gap: 2px;
  min-width: 780px;
}

.room-util-heatmap-corner {
  /* empty top-left cell above the day labels, left of the hour labels */
}

.room-util-heatmap-hour,
.room-util-heatmap-day {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.7rem;
  color: var(--text-muted);
  padding: 2px;
}

.room-util-heatmap-day {
  justify-content: flex-end;
  padding-right: 6px;
  font-weight: 600;
  white-space: nowrap;
}

.room-util-heatmap-cell {
  /* Intensity is set per-cell from JS via
     element.style.setProperty('--intensity', v) — never a hand-built
     style="..." string with interpolated data (web/CLAUDE.md). --primary-color
     is a brand color, constant across light/dark themes, so no separate
     [data-theme="dark"] override is needed here. */
  --intensity: 0;
  aspect-ratio: 1;
  min-width: 1.75rem;
  border-radius: 2px;
  border: 1px solid var(--border-color);
  background-color: rgba(37, 99, 235, var(--intensity));
}

/* ============================================
   In-page error banners (UI-INPAGE-ERRORS-01, flag feature.inpage_errors)
   Utils.showError puts these at the top of the open dialog or the page.
   Only theme tokens, so both themes follow; no separate dark block needed.
   ============================================ */
.inpage-errors {
  position: sticky;
  top: var(--pinned-header-offset, 0px);
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
/* Inside a dialog the banner sticks within .modal-content, which scrolls on
   its own: the page's pinned banners are not above it, so no offset. */
.modal-content .inpage-errors {
  top: 0;
}

.inpage-error {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  background: var(--bg-card);
  color: var(--text-primary);
  border: 1px solid var(--border-color-strong);
  border-left: 4px solid var(--danger-color);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-lg);
}

.inpage-error-text {
  flex: 1;
  overflow-wrap: anywhere;
}

.inpage-error-close {
  flex: none;
  min-width: 2rem;
  min-height: 2rem;
  background: transparent;
  color: var(--text-secondary);
  border: 0;
  border-radius: var(--border-radius);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
}

.inpage-error-close:hover,
.inpage-error-close:focus-visible {
  background: var(--bg-hover);
  color: var(--text-primary);
}

/* ============================================
   Toast Notifications
   ============================================ */
.toast {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  padding: 1rem 1.5rem;
  border-radius: var(--border-radius);
  color: white;
  font-weight: 500;
  box-shadow: var(--shadow-lg);
  z-index: 2000;
  opacity: 0;
  transform: translateY(1rem);
  transition:
    opacity 0.3s,
    transform 0.3s;
  /* The element is reused and stays in the DOM while hidden; without this it
     would keep intercepting clicks in the bottom-right corner at opacity 0. */
  pointer-events: none;
}

.toast.show {
  opacity: 1;
  transform: translateY(0);
}

/* Toasts carry white text, so they need darker backgrounds than the shared
   --secondary-color/--danger-color button tokens: white on #10b981 is only
   2.5:1 and on #ef4444 only 3.8:1, both below the WCAG AA 4.5:1 floor. These
   shades hit 5.5:1 and 6.5:1. Neither token has a dark-theme override, so one
   value is correct in both themes. */
.toast-success {
  background-color: #047857;
}

.toast-error {
  background-color: #b91c1c;
}

.toast-info {
  background-color: var(--primary-color);
}

.toast-warning {
  background-color: var(--warning-color);
  color: var(--gray-900);
}

/* Utils.showReauthPrompt's one interactive toast: its button needs a gap
   from the preceding message text, which every other (text-only) toast
   never needed. */
#reauthPrompt .btn {
  margin-left: 1rem;
}

/* Only THIS toast takes clicks. Text-only toasts stay click-through
   (pointer-events: none on .toast) so a toast never blocks the button under it
   for the seconds it is visible. */
#reauthPrompt.show {
  pointer-events: auto;
}

/* ============================================
   Theme Toggle Button (Quick toggle in header)
   ============================================ */
.theme-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  background-color: var(--bg-card-alt);
  border: 1px solid var(--border-color);
  cursor: pointer;
  transition: var(--transition);
}

.theme-toggle:hover {
  background-color: var(--bg-hover);
}

.theme-toggle .theme-icon {
  font-size: 1.25rem;
}

/* ============================================
   Settings Page Styles
   ============================================ */
.settings-section {
  margin-bottom: 2rem;
}

.setting-group {
  margin-bottom: 1.5rem;
}

.setting-group[hidden] {
  /* explicit, so no author display rule on the group can beat [hidden] */
  display: none;
}

.setting-group h3 {
  margin-bottom: 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border-color);
}

.setting-row {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 1rem;
  align-items: center;
  padding: 0.75rem 0;
}

.setting-row[hidden] {
  /* the grid display above beats the UA [hidden] rule, so hide explicitly */
  display: none;
}

.setting-row label {
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: 0;
}

.setting-description {
  font-size: var(--font-size-sm);
  color: var(--text-muted);
  margin-top: 0.25rem;
}

/* Toggle switch for settings */
.toggle-switch {
  position: relative;
  display: inline-block;
  width: 3rem;
  height: 1.5rem;
}

.toggle-switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: var(--gray-300);
  transition: var(--transition);
  border-radius: 1.5rem;
}

.toggle-slider:before {
  position: absolute;
  content: '';
  height: 1.125rem;
  width: 1.125rem;
  left: 0.1875rem;
  bottom: 0.1875rem;
  background-color: white;
  transition: var(--transition);
  border-radius: 50%;
}

input:checked + .toggle-slider {
  background-color: var(--primary-color);
}

input:checked + .toggle-slider:before {
  transform: translateX(1.5rem);
}

@media (max-width: 768px) {
  .setting-row {
    grid-template-columns: 1fr;
  }

  .toast {
    left: 1rem;
    right: 1rem;
    bottom: 1rem;
  }
}

/* ────────────────────────────────────────────────────────────
   Impersonation banner + picker
   ──────────────────────────────────────────────────────────── */

.impersonation-banner {
  position: sticky;
  /* Sits below the demo banner (see styles.css .top-bar comment above) when
     one is present; --demo-banner-height is absent (0px) otherwise. */
  top: calc(var(--site-banner-height, 0px) + var(--demo-banner-height, 0px));
  left: 0;
  right: 0;
  z-index: 900;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 1rem;
  background: #f59e0b;
  color: #1f2937;
  font-size: 0.92rem;
  font-weight: 500;
  box-shadow: 0 2px 4px rgba(0,0,0,0.08);
  border-bottom: 1px solid #d97706;
}

.impersonation-banner-text { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.impersonation-banner-role {
  background: rgba(255,255,255,0.35);
  border-radius: 4px;
  padding: 0.08rem 0.5rem;
  font-size: 0.78rem;
  text-transform: capitalize;
}
.impersonation-banner-exit {
  background: #1f2937;
  color: #fbbf24;
  border: 0;
  border-radius: 6px;
  padding: 0.45rem 0.9rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}
.impersonation-banner-exit:hover:not(:disabled) { background: #111827; }
.impersonation-banner-exit:disabled { opacity: 0.7; cursor: wait; }

[data-theme="dark"] .impersonation-banner {
  background: #b45309;
  color: #fef3c7;
  border-bottom-color: #78350f;
}
[data-theme="dark"] .impersonation-banner-exit { background: #0f172a; color: #fde68a; }

@media (max-width: 600px) {
  .impersonation-banner { flex-direction: column; align-items: stretch; gap: 0.4rem; text-align: center; }
}

/* ────────────────────────────────────────────────────────────
   Demo-tenant banner (DEMO-TENANT-INDICATOR-LOCK-01, founder feedback #06)
   Inserted by Nav.renderDemoBanner as the first child of <body> — same
   sticky-strip technique as .impersonation-banner above, but visually
   distinct (violet vs. amber) since the two can appear together (a manager
   using View As inside a demo studio). Deliberately NOT hidden at any
   width — "visible at 375px" is the explicit requirement, unlike the
   trial/studio chips this repo already hides under 768px to avoid
   top-bar overflow (see .top-bar's no-wrap note) — which is why this
   lives in its own full-width strip rather than inside .top-bar-right.
   Always the outermost sticky strip (z-index 910, one above the
   impersonation banner's 900) since it's always DOM-first — see the
   .top-bar comment above for the full demo+impersonation+top-bar stacking
   scheme. Both banners are page chrome, so they sit BELOW every overlay
   (sidebar/dropdowns/.modal at 998-1000, .pin-gate at 1100, .toast at
   2000) — see the z-index scale note above .top-bar's @media block.
   ──────────────────────────────────────────────────────────── */
.demo-tenant-banner {
  position: sticky;
  /* Below the tier bar (js/site-banner.js, --site-banner-height; absent = 0px
     on production). WEB-BANNER-STICKY-SIDEBAR-01. */
  top: var(--site-banner-height, 0px);
  left: 0;
  right: 0;
  z-index: 910;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.5rem 1rem;
  background: #7c3aed;
  color: #ffffff;
  font-size: 0.85rem;
  font-weight: 600;
  text-align: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
  border-bottom: 1px solid #5b21b6;
}

.demo-tenant-banner strong {
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

[data-theme="dark"] .demo-tenant-banner {
  background: #4c1d95;
  color: #ede9fe;
  border-bottom-color: #2e1065;
}

@media (max-width: 600px) {
  .demo-tenant-banner { font-size: 0.75rem; padding: 0.4rem 0.6rem; }
}

/* Picker modal */
.impersonation-picker-help {
  font-size: 0.85rem;
  color: var(--text-muted, #6b7280);
  margin-bottom: 0.8rem;
}
.impersonation-results { max-height: 320px; overflow-y: auto; border: 1px solid var(--border-color, #e5e7eb); border-radius: 6px; padding: 0.25rem; }
.impersonation-group + .impersonation-group { margin-top: 0.5rem; }
.impersonation-group-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted, #6b7280);
  padding: 0.4rem 0.5rem 0.2rem;
}
.impersonation-candidate {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
  border-radius: 4px;
}
.impersonation-candidate:hover { background: var(--bg-hover, #f3f4f6); }
.impersonation-candidate-name { font-weight: 500; }
.impersonation-candidate-meta { font-size: 0.82rem; color: var(--text-muted, #6b7280); }
.impersonation-empty, .impersonation-loading {
  padding: 1rem; text-align: center; color: var(--text-muted, #6b7280); font-size: 0.9rem;
}
.impersonation-confirm { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border-color, #e5e7eb); }
.impersonation-confirm-target { margin-bottom: 0.75rem; font-size: 0.95rem; }

/* ============================================================
   Platform Config page (admin-platform-config.html)
   All colors reference existing themed CSS variables, so dark
   mode is inherited automatically via [data-theme="dark"].
   ============================================================ */

/* Monospace cell for config keys and raw values */
.pc-key,
.pc-readonly-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  word-break: break-all;
}

.pc-readonly-value {
  color: var(--text-secondary);
}

/* Inline edit row: input + buttons on one line, wraps on narrow screens */
.pc-edit-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

/* Override the global full-width input default for the compact inline edit */
.pc-edit-input {
  width: auto;
  min-width: 8rem;
  max-width: 16rem;
  flex: 1 1 8rem;
  padding: 0.4rem 0.6rem;
  font-size: 0.85rem;
}

/* Source provenance badge — reuses themed soft-color variables */
.pc-source {
  display: inline-block;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  background-color: var(--bg-neutral-soft);
  color: var(--text-neutral-strong);
}
.pc-source-tenant {
  background-color: var(--bg-info-soft);
  color: var(--text-info-strong);
}
.pc-source-platform {
  background-color: var(--bg-success-soft);
  color: var(--text-success-strong);
}
.pc-source-env {
  background-color: var(--bg-warning-soft);
  color: var(--text-warning-strong);
}

/* "bounce / restart required" badge */
.pc-bounce {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 600;
  background-color: var(--bg-warning-soft);
  color: var(--text-warning-strong);
}

/* Muted note for env-only rows */
.pc-note {
  display: block;
  margin-top: 0.25rem;
  font-size: 0.75rem;
  color: var(--text-secondary);
}

/* Quota readout */
.pc-quota-current {
  margin: 0.75rem 0;
  font-size: 0.95rem;
  color: var(--text-secondary);
}
.pc-quota-current strong {
  color: var(--text-primary);
}

/* ============================================================
   Messaging — thread-view spike (mobile-first @375px)
   List <-> Thread are two states on mobile; on >=768px a
   two-pane layout shows both. All colors come from theme
   tokens (auto dark-mode) except the message bubbles, which
   get explicit dark overrides below.
   ============================================================ */

/* Layout container. On mobile, exactly one pane is shown at a
   time via the .msg-show-thread state toggle on the wrapper. */
.msg-wrap {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* ---- Conversation list ---- */
.msg-list {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  box-shadow: var(--shadow);
  overflow: hidden;
}

.msg-list-row {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border-color);
  padding: 0.85rem 1rem;
  cursor: pointer;
  font: inherit;
  color: var(--text-primary);
}
.msg-list-row:last-child {
  border-bottom: none;
}
.msg-list-row:hover,
.msg-list-row:focus-visible {
  background: var(--bg-hover);
  outline: none;
}

.msg-list-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
}
.msg-list-subject {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg-list-time {
  flex-shrink: 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.msg-list-snippet {
  margin-top: 0.25rem;
  font-size: 0.85rem;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg-list-meta {
  margin-top: 0.25rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* Unread dot before the subject. */
.msg-unread-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--primary-color);
  margin-right: 0.4rem;
  vertical-align: middle;
}

/* ---- Thread pane ---- */
.msg-thread {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  min-height: 60vh;
}

.msg-thread-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border-color);
}
.msg-back-btn {
  background: none;
  border: none;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  color: var(--text-secondary);
  padding: 0.25rem 0.5rem 0.25rem 0;
}
.msg-thread-title {
  font-weight: 600;
  font-size: 1rem;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Participants bar — role + first name only, never contact info. */
.msg-participants {
  padding: 0.5rem 1rem;
  border-bottom: 1px solid var(--border-color);
  font-size: 0.8rem;
  color: var(--text-secondary);
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.msg-participant {
  background: var(--bg-card-alt);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  white-space: nowrap;
}
.msg-participant-observer {
  font-style: italic;
  color: var(--text-muted);
}

/* Message scroll area */
.msg-messages {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.msg-bubble {
  max-width: 80%;
  padding: 0.55rem 0.8rem;
  border-radius: 12px;
  font-size: 0.9rem;
  line-height: 1.4;
  word-wrap: break-word;
  overflow-wrap: anywhere;
}
.msg-bubble-meta {
  display: block;
  font-size: 0.72rem;
  margin-bottom: 0.2rem;
  opacity: 0.85;
}
.msg-bubble-body {
  white-space: pre-wrap;
}

/* Studio-side (teacher/manager/initiator) — left aligned. */
.msg-bubble-studio {
  align-self: flex-start;
  background: var(--bg-card-alt);
  color: var(--text-primary);
  border-bottom-left-radius: 3px;
}
/* External (guardian/student/payer) — right aligned, brand tint. */
.msg-bubble-external {
  align-self: flex-end;
  background: var(--primary-light);
  color: var(--gray-900);
  border-bottom-right-radius: 3px;
}

/* Day divider */
.msg-day-divider {
  align-self: center;
  font-size: 0.72rem;
  color: var(--text-muted);
  margin: 0.4rem 0;
}

/* Composer */
.msg-composer {
  border-top: 1px solid var(--border-color);
  padding: 0.75rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.msg-composer textarea {
  width: 100%;
  resize: vertical;
  min-height: 2.6rem;
  max-height: 8rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border-color-strong);
  border-radius: 8px;
  background: var(--bg-input);
  color: var(--text-primary);
  font: inherit;
}
.msg-composer-row {
  display: flex;
  justify-content: flex-end;
}

/* Empty / loading states inside panes */
.msg-empty {
  padding: 2.5rem 1rem;
  text-align: center;
  color: var(--text-muted);
  font-size: 0.9rem;
}

/* Mobile: only one pane visible at a time. */
@media (max-width: 767px) {
  .msg-list-pane,
  .msg-thread-pane {
    display: none;
  }
  .msg-wrap:not(.msg-show-thread) .msg-list-pane {
    display: block;
  }
  .msg-wrap.msg-show-thread .msg-thread-pane {
    display: block;
  }
}

/* Wider screens: two-pane, back button hidden. */
@media (min-width: 768px) {
  .msg-wrap {
    flex-direction: row;
    align-items: flex-start;
  }
  .msg-list-pane {
    flex: 0 0 20rem;
  }
  .msg-thread-pane {
    flex: 1;
    min-width: 0;
  }
  .msg-back-btn {
    display: none;
  }
}

/* Dark-mode bubble overrides (the only non-token colors above). */
[data-theme='dark'] .msg-bubble-external {
  background: #1e3a8a;
  color: #f8fafc;
}

/* Visually-hidden label (a11y) — keeps screen readers happy without
   showing the text. Used by the messaging composer textarea label. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================
   Malformed phone numbers
   A stored phone that predates the NANP write-validation (backend
   `nanp` binding tag) cannot be saved again until it is corrected. Rather
   than let an operator discover that as a 400 on Save, the number announces
   itself wherever it is displayed and wherever it is loaded into a form.
   Colours reuse --bg-warning-soft / --text-warning-strong, which already
   carry dark-mode overrides, so no new dark rule is needed here.
   ========================================================================== */

.phone-invalid {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.05rem 0.4rem;
  border-radius: 4px;
  background-color: var(--bg-warning-soft);
  color: var(--text-warning-strong);
  cursor: help;
}

.phone-invalid-icon {
  font-size: 0.85em;
  line-height: 1;
}

/* Applied to a form input whose current value can't be saved. Uses a border
   plus the warning tint rather than colour alone, so the state does not
   depend on colour vision. */
.input-invalid {
  border-color: var(--warning-color) !important;
  background-color: var(--bg-warning-soft);
}

.phone-field-warning {
  margin-top: 0.3rem;
  font-size: 0.8rem;
  line-height: 1.35;
  color: var(--text-warning-strong);
}

/* Guardian/payer search results (students.html).
   WEB-INLINE-HANDLER-ESCAPEHTML-XSS-01: these rows are JS hooks for a delegated
   click listener. The hover state lives here rather than in onmouseover/onmouseout
   so the row carries no inline handlers at all. */
.user-search-result {
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  border-radius: 4px;
  margin-bottom: 2px;
  transition: background 0.15s;
}

.user-search-result:hover,
.user-search-result:focus-visible {
  background: var(--bg-card-alt);
}

.user-search-name {
  font-weight: 500;
}

.user-search-email {
  font-size: 0.8rem;
  color: var(--text-muted);
}


/* ==========================================================================
   Public auth chrome (login, join, intake, invite, signup, activate,
   reset-password, payment result pages)
   Solid brand field. Colors are tokens so the card follows the theme.
   ========================================================================== */

body.auth-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background-color: var(--primary-dark);
  padding: 2rem 1rem;
}

body.auth-page.auth-page--top {
  align-items: flex-start;
}

.login-container,
.join-container,
.intake-container,
.invite-container,
.signup-container,
.activate-container,
.reset-container,
.payment-container {
  background: var(--bg-card);
  color: var(--text-primary);
  border-radius: 12px;
  box-shadow: var(--shadow-lg);
  width: 100%;
}

.login-container,
.activate-container,
.reset-container {
  padding: 3rem 2rem;
  max-width: 400px;
}

.signup-container {
  padding: 2.5rem 2rem;
  max-width: 440px;
}

.payment-container {
  padding: 3rem 2rem;
  max-width: 440px;
  text-align: center;
}

.join-container,
.intake-container,
.invite-container {
  padding: 2.5rem 2rem;
}

.join-container {
  max-width: 560px;
  margin-top: 1rem;
}

.intake-container,
.invite-container {
  max-width: 480px;
}

.invite-container {
  margin-top: 1rem;
}

.login-header,
.join-header,
.intake-header,
.invite-header,
.signup-header,
.activate-header,
.reset-header {
  text-align: center;
  margin-bottom: 1.5rem;
}

.login-header,
.activate-header,
.reset-header {
  margin-bottom: 2rem;
}

.login-header h1,
.join-header h1,
.intake-header h1,
.invite-header h1,
.signup-header h1,
.activate-header h1,
.reset-header h1 {
  margin-bottom: 0.25rem;
}

.signup-header h1 {
  font-size: 1.6rem;
}

.join-header h1,
.intake-header h1 {
  font-size: 1.5rem;
}

.invite-header h1 {
  font-size: 1.4rem;
}

.login-header h1,
.activate-header h1,
.reset-header h1 {
  margin-bottom: 0.5rem;
}

.login-header .subtitle,
.join-header .subtitle,
.intake-header .subtitle,
.invite-header .subtitle,
.signup-header .subtitle,
.activate-header .subtitle,
.reset-header .subtitle {
  color: var(--text-muted);
}

.signup-header .subtitle {
  font-size: 0.95rem;
}

.join-header .subtitle,
.intake-header .subtitle,
.invite-header .subtitle {
  font-size: 0.95rem;
}

.join-header .subtitle {
  white-space: pre-wrap;
}

.login-form button,
.join-form button[type='submit'],
.intake-form button[type='submit'],
.invite-form button[type='submit'],
.signup-form button[type='submit'] {
  width: 100%;
}

.join-form button[type='submit'],
.intake-form button[type='submit'],
.invite-form button[type='submit'],
.signup-form button[type='submit'] {
  margin-top: 0.5rem;
}

.join-form .form-group,
.intake-form .form-group,
.invite-form .form-group,
.signup-form .form-group {
  margin-bottom: 1rem;
}

.join-form label,
.intake-form label,
.invite-form label,
.signup-form label {
  display: block;
  margin-bottom: 0.25rem;
  font-weight: 600;
  font-size: 0.9rem;
}

.join-form input:not([type='checkbox']),
.join-form select,
.join-form textarea,
.intake-form input,
.intake-form textarea,
.invite-form input:not([type='checkbox']),
.signup-form input:not([type='checkbox']) {
  width: 100%;
  padding: 0.65rem 0.75rem;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  font-size: 0.95rem;
  background-color: var(--bg-input);
  color: var(--text-primary);
}

.invite-form input[readonly] {
  background: var(--bg-card-alt);
  color: var(--text-muted);
}

.login-error,
.join-error,
.invite-error,
.intake-error,
.signup-error,
.activate-error,
.reset-error {
  color: var(--text-danger-strong);
  margin-bottom: 1rem;
  padding: 0.75rem;
  background: var(--bg-danger-soft);
  border-radius: 4px;
  font-size: 0.9rem;
}

/* Hidden only while EMPTY. A box holding a message shows, however the code
   toggles it: much of it sets the text then clears the inline style
   (style.display = ''), which a plain display:none here silently overrode, so
   "Incorrect PIN" and the invite/join errors were set but never seen
   (Ed, round 3, 2026-10-08). An explicit inline display:none still hides.
   Held by web/tools/test-error-boxes-show-when-filled.js. */
.login-error:empty,
.join-error:empty,
.invite-error:empty,
.signup-error:empty,
.activate-error:empty,
.reset-error:empty {
  display: none;
}

.login-error.is-success,
.signup-error.is-success,
.activate-success,
.reset-success {
  color: var(--text-success-strong);
  background: var(--bg-success-soft);
}

.activate-success,
.reset-success {
  margin-bottom: 1rem;
  padding: 0.75rem;
  border-radius: 4px;
  display: none;
}

.name-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}

.trial-badge {
  display: inline-block;
  background: var(--bg-success-soft);
  color: var(--text-success-strong);
  padding: 0.25rem 0.75rem;
  border-radius: 12px;
  font-size: 0.8rem;
  font-weight: 600;
  margin-bottom: 0.5rem;
}

.login-link {
  text-align: center;
  margin-top: 1.5rem;
  font-size: 0.9rem;
  color: var(--text-muted);
}

.login-link a,
.back-link,
.resend-link a {
  color: var(--primary-color);
}

.login-link a,
.resend-link a {
  text-decoration: underline;
}

.back-link {
  display: block;
  text-align: center;
  margin-top: 1.5rem;
  text-decoration: none;
  font-size: 0.85rem;
}

.resend-link {
  text-align: center;
  margin-top: 0.75rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}

.resend-link a {
  cursor: pointer;
}

.channel-picker {
  display: flex;
  gap: 1rem;
  margin-bottom: 1rem;
}

.channel-picker label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.85rem;
  cursor: pointer;
}

.code-input {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  font-size: 1.5rem;
  text-align: center;
  letter-spacing: 0.5rem;
  font-family: monospace;
  box-sizing: border-box;
  background-color: var(--bg-input);
  color: var(--text-primary);
}

.payment-brand {
  font-size: 1.5rem;
  font-weight: 700;
  margin-bottom: 2rem;
  color: var(--text-primary);
}

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

.payment-message {
  color: var(--text-muted);
  line-height: 1.6;
  margin-bottom: 0.5rem;
}

.payment-note {
  color: var(--text-muted);
  font-size: 0.875rem;
  margin-bottom: 2rem;
}

.payment-container .btn {
  display: inline-block;
  text-decoration: none;
}

.status-icon {
  width: 72px;
  height: 72px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 1.5rem;
}

.status-icon--success {
  background: var(--bg-success-soft);
}

.status-icon--success::before {
  content: '';
  display: block;
  width: 28px;
  height: 16px;
  border-left: 4px solid var(--text-success-strong);
  border-bottom: 4px solid var(--text-success-strong);
  transform: rotate(-45deg);
  margin-top: -4px;
}

.status-icon--warning {
  background: var(--bg-warning-soft);
}

.status-icon--warning::before {
  content: '!';
  font-size: 2rem;
  font-weight: 700;
  color: var(--text-warning-strong);
  line-height: 1;
}

.auth-aux {
  text-align: center;
  margin-top: 0.75rem;
}

.auth-aux a {
  color: var(--primary-color);
  font-size: 0.85rem;
  text-decoration: none;
}

.auth-aux a:hover {
  text-decoration: underline;
}

.auth-aux-sep {
  color: var(--text-muted);
  font-size: 0.85rem;
  margin: 0 0.35rem;
}

.auth-studio-title {
  margin-bottom: 1rem;
}

.auth-back {
  width: 100%;
  margin-top: 1rem;
}

/* Signup prompt under the login form. Class name is historical; this is
   the create-studio link, not a demo-credential picker. */
.demo-section {
  margin-top: 2rem;
  padding-top: 2rem;
  border-top: 1px solid var(--border-color);
  text-align: center;
}

.demo-section h3 {
  font-size: 0.95rem;
  color: var(--text-muted);
  margin-bottom: 1rem;
}

.auth-signup-btn {
  margin-bottom: 0.75rem;
}

.auth-signup-note {
  font-size: 0.85rem;
}

.demo-buttons {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.demo-btn {
  padding: 0.75rem;
  border: 1px solid var(--border-color);
  background: var(--bg-card);
  color: var(--text-primary);
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.875rem;
  font-family: inherit;
  transition: var(--transition);
}

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

.auth-hint {
  font-size: 0.8rem;
  color: var(--text-muted);
  margin: -0.5rem 0 1rem;
}

.auth-hint--flush {
  margin-bottom: 0;
}

.join-two-col,
.intake-two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}

.join-tabs,
.invite-tabs {
  display: flex;
  gap: 0;
  margin-bottom: 1.25rem;
  border-bottom: 2px solid var(--border-color);
}

.join-tabs button,
.invite-tabs button {
  flex: 1;
  background: none;
  border: none;
  padding: 0.65rem 0.5rem;
  border-bottom: 3px solid transparent;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--text-muted);
}

.join-tabs button.active,
.invite-tabs button.active {
  color: var(--primary-color);
  border-bottom-color: var(--primary-color);
}

.child-row,
.invite-row {
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 0.85rem;
  margin-bottom: 0.75rem;
  position: relative;
  background: var(--bg-card-alt);
}

.child-row-remove,
.invite-row-remove {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
}

.join-success,
.intake-success,
.invite-success {
  text-align: center;
  padding: 1rem 0;
}

.join-success h2,
.intake-success h2,
.invite-success h2 {
  margin-bottom: 0.5rem;
}

#joinUnavailable,
#joinAlreadyMember,
#intakeUnavailable,
#inviteUnavailable {
  text-align: center;
  color: var(--text-muted);
}

.policy-text-box {
  max-height: 180px;
  overflow-y: auto;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  padding: 0.75rem;
  font-size: 0.85rem;
  color: var(--text-secondary);
  white-space: pre-wrap;
  margin-bottom: 0.75rem;
  background: var(--bg-card-alt);
}

.mine-list {
  margin-top: 1rem;
  text-align: left;
}

.mine-list .list-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--border-color);
  padding: 0.6rem 0;
  font-size: 0.9rem;
}

.invite-signin-note {
  font-size: 0.85rem;
  color: var(--text-muted);
  margin-bottom: 0.75rem;
}

/* Join, intake, and invite used a flat consent row. Signup and activate
   keep the shared bordered .consent-group box. */
body.auth-page .join-container .consent-group,
body.auth-page .intake-container .consent-group,
body.auth-page .invite-container .consent-group {
  margin: 1.25rem 0;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 0;
  font-size: 0.85rem;
  color: var(--text-muted);
}

@media (max-width: 480px) {
  .join-two-col,
  .intake-two-col {
    grid-template-columns: 1fr;
  }
}

/* Shared filter label (invoices and any page that repeated the inline style). */
.filter-label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
  color: var(--text-secondary);
}

/* Inline error / success banners. display stays on the element so callers
   that clear style.display (empty string) still reveal the box. */
.inline-error {
  color: var(--text-danger-strong);
  margin-bottom: 1rem;
  padding: 0.6rem;
  background: var(--bg-danger-soft);
  border-radius: 4px;
  font-size: 0.9rem;
}

.inline-success {
  margin-top: 0.5rem;
  padding: 0.5rem 0.75rem;
  background: var(--bg-success-soft);
  color: var(--text-success-strong);
  border-radius: 6px;
  font-size: 0.85rem;
}

.info-callout {
  margin-bottom: 1rem;
  padding: 0.75rem 1rem;
  background: var(--bg-info-soft);
  border: 1px solid var(--primary-color);
  border-radius: 4px;
  color: var(--text-info-strong);
}

.info-callout a {
  color: var(--primary-color);
  font-weight: 600;
  margin-left: 0.5rem;
}

.chip {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 4px;
  font-size: 0.75rem;
  font-weight: 600;
}

.chip-group {
  background: var(--group-accent-hover);
  color: var(--group-accent);
}

.chip-lesson {
  background: var(--bg-info-soft);
  color: var(--text-info-strong);
}

.late-badge {
  background: var(--bg-warning-soft);
  color: var(--text-warning-strong);
  padding: 0.1rem 0.4rem;
  border-radius: 3px;
  font-size: 0.7rem;
  font-weight: 600;
  margin-left: 0.3rem;
}

.closure-note {
  font-size: 0.65rem;
  color: var(--danger-color);
  padding: 0.2rem 0.25rem;
}

.audit-delta-old {
  color: var(--danger-color);
}

.audit-delta-new {
  color: var(--secondary-color);
}

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

.btn-warning:hover {
  background: var(--warning-color);
  filter: brightness(0.92);
}

.summary-count--ok {
  color: var(--text-success-strong);
}

.summary-count--warn {
  color: var(--text-warning-strong);
}

.summary-count--bad {
  color: var(--text-danger-strong);
}

.summary-count--info {
  color: var(--primary-color);
}

.note-muted {
  font-size: 0.75rem;
  color: var(--primary-color);
  font-style: italic;
}

/* UI-STATUS-TABS-01 (flag feature.status_tabs): tabs over the Students and
   Guardians lists, and the per-row status menu. Colors come from the theme
   variables, so dark mode follows without extra overrides. */
.status-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--border-color);
}
.status-tab {
  background: none;
  border: 0;
  border-bottom: 3px solid transparent;
  color: var(--text-secondary);
  cursor: pointer;
  font: inherit;
  font-weight: 600;
  min-height: 44px;
  padding: 0.5rem 1rem;
}
.status-tab:hover {
  color: var(--text-primary);
}
.status-tab.active {
  border-bottom-color: var(--primary-color, #4f46e5);
  color: var(--primary-color, #4f46e5);
}
.status-tab:focus-visible,
.status-menu-trigger:focus-visible,
.status-menu-item:focus-visible {
  outline: 2px solid var(--primary-color, #4f46e5);
  outline-offset: 2px;
}
.status-tab-count {
  color: var(--text-muted);
  font-weight: 400;
}
button.status-menu-trigger::after {
  content: ' \25BE';
}
button.status-menu-trigger {
  border: 0;
  cursor: pointer;
  font: inherit;
  font-size: inherit;
}
@media (max-width: 768px) {
  button.status-menu-trigger {
    min-height: 44px;
  }
}
.status-menu {
  background: var(--bg-card);
  border: 1px solid var(--border-color-strong);
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  min-width: 11rem;
  padding: 0.25rem;
  position: absolute;
  z-index: 1700;
}
.status-menu-item {
  background: none;
  border: 0;
  border-radius: 6px;
  color: var(--text-primary);
  cursor: pointer;
  display: block;
  font: inherit;
  min-height: 44px;
  padding: 0.5rem 0.75rem;
  text-align: left;
  width: 100%;
}
.status-menu-item:hover {
  background: var(--bg-hover);
}

/* UI-UNSAVED-INDICATORS-01 (flag feature.unsaved_indicators): a changed-but-
   unsaved field and its "Was: ..." note, and a Save button with nothing to
   save. Tokens only: the warning tokens already flip in dark mode, so no
   separate [data-theme="dark"] block is needed. */
.field-unsaved {
  border-color: var(--warning-color) !important;
  box-shadow: 0 0 0 2px var(--bg-warning-soft);
}
.field-unsaved-note {
  background: var(--bg-warning-soft);
  border-radius: 4px;
  color: var(--text-warning-strong);
  display: block;
  font-size: var(--font-size-xs);
  margin-top: 0.25rem;
  overflow-wrap: anywhere;
  padding: 0.125rem 0.375rem;
  white-space: pre-wrap;
}
[data-qs-unsaved-disabled] {
  cursor: not-allowed;
  opacity: 0.55;
}
