/* Snowy Studio -- hand-written stylesheet (no framework, no build step).
   Theming via CSS custom properties. Dark mode by OS preference only. */

/* ----------------------------------------------------------------- Tokens */
:root {
  --color-bg: #ffffff;
  --color-surface: #f9fafb;
  --color-surface-2: #f3f4f6;
  /* The fill an interactive element tints to under the pointer, and the one
     surface token added for a role rather than for a depth. --color-surface-2
     is a *resting* fill -- a pill, a .btn face, a table header, the current nav
     link -- and in dark mode it is too light to carry the accent ink family:
     --color-primary reads 2.82:1 on it and --color-primary-text 4.07:1, both
     under the 4.5:1 text owes. Three rules put an accent ink on an element
     whose own :hover tints it (a link in a .clickable-row, .app-nav-upgrade
     under .app-nav-link:hover, .meeting-card-time under .meeting-card:hover),
     so the tint has to be a fill those inks can be read on, and no ink in the
     primary family answers --color-surface-2 at text size.
     Darker than --color-surface-2 rather than lighter: dark
     --color-primary-text is #60a5fa, a light blue, so raising its contrast
     means lowering the fill's luminance. Here it is 6.09:1 / 4.72:1,
     --color-text 16.12 / 10.95 and --color-text-muted 5.25 / 5.42. Every one
     of the six hover rules still *lightens* its element in dark mode -- 1.22:1
     against a .card where --color-surface-2 was 1.41:1 -- so the affordance
     keeps its direction and gives up some of its strength, which is the price
     of the ink on it being readable. Light mode had no defect (6.09:1) and
     takes the same value --color-surface-2 has, so nothing there moves on
     screen. T65; graded in tests/test_contrast.py. */
  --color-surface-hover: #f3f4f6;
  --color-text: #111827;
  --color-text-muted: #5f6673;
  --color-border: #d1d5db;
  /* Control boundaries need 3:1 (WCAG 1.4.11); --color-border is decorative. */
  --color-border-strong: #767f8c;
  --color-primary: #2563eb;
  --color-primary-hover: #1d4ed8;
  --color-danger: #dc2626;
  --color-danger-hover: #b91c1c;
  --color-success: #16a34a;
  --color-warning: #d97706;
  --color-info: #2563eb;

  /* Accent fills that sit behind text, and the one ink that goes on them.
     Light mode inks white, so the fills are darkened to clear 4.5:1; dark mode
     inverts this (see below) because its accents are too bright to carry white. */
  --color-on-fill: #ffffff;
  --color-primary-fill: #2563eb;
  --color-primary-fill-hover: #1d4ed8;
  --color-danger-fill: #dc2626;
  --color-danger-fill-hover: #b91c1c;
  --color-success-fill: #15803d;
  --color-warning-fill: #b45309;
  --color-info-fill: #2563eb;

  /* Accent text on plain or lightly tinted backgrounds (pills, flags, inline
     status). Darker than the fill tokens, which are too light to read as text. */
  --color-primary-text: #1d4ed8;
  --color-danger-text: #b91c1c;
  --color-success-text: #116932;
  --color-warning-text: #a84a06;

  --radius: 10px;
  --radius-sm: 6px;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.12);
  --sidebar-w: 240px;
  --maxw: 1100px;
  /* The smallest box an icon-only control may be pressed in. WCAG 2.5.8 Target
     Size (Minimum) asks for 24x24 CSS px; this is 44px, the platform touch
     minimum (and WCAG 2.5.5 AAA), so the controls whose entire content is a
     glyph clear the floor with room rather than sitting on it -- .app-menu-btn
     was 30x10 and .app-menu-close reached 32.8x27.2 by accident. The mark
     inside each one keeps the size it had: the target grows around it.
     tests/test_tap_targets.py checks the 24px floor against this stylesheet. */
  --tap-min: 44px;
  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0f172a;
    --color-surface: #1e293b;
    --color-surface-2: #334155;
    --color-surface-hover: #2b374b;
    --color-text: #f1f5f9;
    --color-text-muted: #9fb0c4;
    --color-border: #334155;
    --color-border-strong: #748199;
    --color-primary: #3b82f6;
    --color-primary-hover: #60a5fa;
    --color-danger: #ef4444;
    --color-danger-hover: #f87171;
    --color-success: #22c55e;
    --color-warning: #f59e0b;
    --color-info: #60a5fa;

    /* Dark accents are bright, so the ink flips to the page color rather than
       white -- white on #3b82f6 is only 3.68:1, while #0f172a on it is 4.85:1. */
    --color-on-fill: #0f172a;
    --color-primary-fill: #3b82f6;
    --color-primary-fill-hover: #60a5fa;
    --color-danger-fill: #ef4444;
    --color-danger-fill-hover: #f87171;
    --color-success-fill: #22c55e;
    --color-warning-fill: #f59e0b;
    --color-info-fill: #60a5fa;

    --color-primary-text: #60a5fa;
    --color-danger-text: #f87171;
    --color-success-text: #22c55e;
    --color-warning-text: #f59e0b;
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.5);
  }
}

/* ------------------------------------------------------------------ Reset */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--font);
  background-color: var(--color-bg);
  color: var(--color-text);
  line-height: 1.5;
  min-height: 100vh;
}

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

/* ------------------------------------------------------------------ Focus */
/* The form controls below get their own ring; everything else a keyboard can
   land on -- every link, every button, every `.btn`, the whole sidebar nav --
   used to fall back to whatever ring the browser happened to draw, which on a
   filled `.btn-primary` can land at poor contrast. One rule, the same 2px
   primary outline the controls use, so the indicator is the product's own.
   `:focus-visible` rather than `:focus`: a mouse click must not leave a ring
   behind. `outline-offset` is 2px rather than the controls' 1px because a
   `.btn-primary` face is `--color-primary` itself -- the ring has to clear the
   fill to be seen at all. One surface this ring's color cannot clear 3:1 on in
   dark mode is `--color-surface-2`, so the one control that sits in a pad of
   it overrides `outline-color`: see `.billing-toggle .toggle-opt`. */
a:focus-visible,
button:focus-visible,
.btn:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* The main landmarks carry `tabindex="-1"` so the skip link can actually move
   focus into them, which brings them under the rule above -- and a visitor who
   just jumped there should see where they landed. But a landmark is as wide as
   the viewport, so a ring drawn 2px *outside* it has its left and right edges
   off screen: what you see is two stray horizontal rules rather than a box.
   Inset it instead, into the padding these regions already have, so the whole
   indicator is on screen. The ring is kept, not removed. The selector repeats
   the `[tabindex]` the rule above matches on: `main:focus-visible` alone is
   (0,1,1) against that rule's (0,2,0) and loses. */
main[tabindex]:focus-visible { outline-offset: -2px; }

/* The same move, for the same reason, on the three panels focus is pushed into
   programmatically. The two `role="dialog"` panels and the off-canvas sidebar
   all carry `tabindex="-1"` so static/js/modal-focus.js can focus them, which
   brings them under the shell rule above -- and none of the three sits on a
   fill. Each is painted over a translucent wash: `.modal-overlay` is
   `rgba(0, 0, 0, 0.5)` and `.app-menu-backdrop` is `rgba(0, 0, 0, 0.45)`, both
   over whatever page is behind them. Composited over light --color-bg that is
   a mid gray -- Chromium paints the dialog wash #7f7f7f -- and mid gray is the
   worst background an accent band can have: --color-primary measures 1.29:1 on
   it, less than half of 1.4.11's 3:1, so for a keyboard visitor opening a
   dialog in light mode the ring was effectively not there. Dark mode was never
   the problem (5.35:1 -- the wash goes nearly black).

   Recoloring it the way `.billing-toggle .toggle-opt` and `.clickable-row:hover
   a` do cannot answer this one: on mid gray nothing but a near-black or a
   near-white clears 3:1, and --color-primary-text is 1.67:1 there, so the
   substitution those two use would have to give up the product's own indicator
   color rather than darken it. Moving the band does answer it, with nothing
   asked of the palette: inset 4px, which leaves the 2px band 1px clear of each
   panel's own 1px hairline border and well inside the padding all three already
   have (1.5rem on .modal, 1rem on .app-sidebar), so both of the band's
   neighbors are the panel's own --color-surface and --color-primary is 4.95:1
   light / 3.98:1 dark on it -- the pair tests/test_contrast.py grades as "the
   ring band on a card". The ring is kept, not removed, and it keeps its color.

   It also puts the sidebar's ring on screen for the first time. The open panel
   is `inset: 0 auto 0 0`, so three of the four sides of a band drawn 2px
   *outside* it are off the viewport -- which is the landmark problem above,
   arriving from a fixed panel instead of a full-width region.

   Specificity: (0,2,0) each, the same as the shell rule's
   `[tabindex]:focus-visible`, so this has to stay *below* it to win. Measured
   in tests/e2e/test_color.py, both schemes, all three panels. */
.modal:focus-visible,
.app-sidebar:focus-visible { outline-offset: -4px; }

/* Links. --color-primary-text rather than --color-primary, because a link
   states no background of its own and the panels it most often lands on are
   --color-surface -- a .card, a .toast, a .modal, the marketing alt/stats/cta
   bands -- where the brighter token measures 4.95:1 in light and only 3.98:1
   in dark, under the 4.5:1 text owes (T64). This one is 6.41:1 / 5.75:1 on
   that fill and 6.70:1 / 7.02:1 on the page, and it is the token this design
   system already nominates for accent ink that has to be read: the same
   substitution the current nav link's edge bar, `.billing-toggle .toggle-opt`
   and `.clickable-row:hover a` make for an indicator. The focus ring on a link
   is still --color-primary -- that is a 3:1 non-text claim and is unaffected.
   The third surface a link reaches is a hover tint -- `.clickable-row:hover`
   paints one under the link in a table's first cell -- and that is
   --color-surface-hover rather than --color-surface-2 for exactly this ink's
   sake: 4.72:1 instead of 4.07:1 in dark mode (T65).
   All three pairs are graded in tests/test_contrast.py. */
a { color: var(--color-primary-text); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3 { line-height: 1.2; font-weight: 700; }
h1 { font-size: 1.75rem; }
h2 { font-size: 1.375rem; }
h3 { font-size: 1.125rem; }

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

/* ---------------------------------------------------------------- Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.55rem 1rem;
  font-size: 0.95rem;
  font-weight: 600;
  font-family: inherit;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  background: var(--color-surface-2);
  color: var(--color-text);
  transition: background-color 0.15s, border-color 0.15s, opacity 0.15s;
}
.btn:hover { text-decoration: none; }
.btn-primary { background: var(--color-primary-fill); color: var(--color-on-fill); }
.btn-primary:hover { background: var(--color-primary-fill-hover); }
.btn-danger { background: var(--color-danger-fill); color: var(--color-on-fill); }
.btn-danger:hover { background: var(--color-danger-fill-hover); }
.btn-ghost { background: transparent; border-color: var(--color-border-strong); color: var(--color-text); }
.btn-ghost:hover { background: var(--color-surface-hover); }
.btn-lg { padding: 0.75rem 1.4rem; font-size: 1.05rem; }
.btn-block { width: 100%; }

/* ------------------------------------------------------------------ Cards */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 1.5rem;
  box-shadow: var(--shadow);
}
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1rem;
}
.notice { border-left: 4px solid var(--color-primary); }

.stat-card { display: flex; flex-direction: column; gap: 0.35rem; }
.stat-label { font-size: 0.8rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.stat-value { font-size: 1.4rem; font-weight: 700; }

/* ------------------------------------------------------------------ Forms */
.stack { display: flex; flex-direction: column; gap: 1rem; }
.form-group { display: flex; flex-direction: column; gap: 0.35rem; }
.form-group label { font-size: 0.9rem; font-weight: 600; }
input[type="text"], input[type="email"], input[type="password"],
input[type="number"], input[type="tel"], input[type="search"],
input[type="date"], input[type="time"], select, textarea {
  width: 100%;
  padding: 0.6rem 0.75rem;
  font-size: 0.95rem;
  font-family: inherit;
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: 1px;
  border-color: var(--color-primary);
}
/* --color-danger-text, not --color-danger: Django renders this list inside the
   `.card` every staff form sits in, where the plain danger token is 4.62:1 in
   light and 3.89:1 in dark. 6.19:1 / 5.29:1 instead (T64). */
.errorlist { list-style: none; color: var(--color-danger-text); font-size: 0.85rem; }
ul.errorlist li { margin-top: 0.2rem; }

/* ------------------------------------------------------- Marketing layout */
.site-nav {
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
  position: sticky;
  top: 0;
  z-index: 20;
}
.site-nav-inner, .site-footer-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0.75rem 1.25rem;
  display: flex;
  align-items: center;
  gap: 1rem;
}
.site-nav-links { display: flex; gap: 1rem; margin-left: 1rem; }
.site-nav-links a { color: var(--color-text); }
/* The current page in the header nav. Weight plus an underline rather than a
   color change: these links already sit at --color-text, so there is no
   darker ink to move to, and two non-color signals satisfy WCAG 1.4.1 on
   their own. */
.site-nav-links a[aria-current="page"] {
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 4px;
}
.site-nav-actions { margin-left: auto; display: flex; align-items: center; gap: 0.6rem; }
/* The header stacks instead of running off the screen. One unwrapped flex row
   holding the brand, four nav links and two action buttons needs about 640px,
   so below that the row was simply wider than the viewport: at 390px the
   document measured 616px and both actions -- including the only conversion
   control on this shell -- sat entirely off screen, which is WCAG 1.4.10
   (Reflow). Wrapping rather than a disclosure, so the tab order is unchanged
   and nothing new has to be opened to reach a nav link. 820px is the
   breakpoint the staff shell's sidebar already uses; the row only actually
   wraps below roughly 745px, so this is the safety net around that point
   rather than a second layout. The margins go because `margin-left: auto` on
   the actions would right-align a row of its own against left-aligned rows
   above it. */
@media (max-width: 820px) {
  .site-nav-inner { flex-wrap: wrap; row-gap: 0.5rem; }
  /* And the four links wrap among themselves below about 320px, where the row
     is exactly as wide as the content box with nothing to spare. */
  .site-nav-links { margin-left: 0; flex-wrap: wrap; }
  .site-nav-actions { margin-left: 0; }
}
.brand { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--color-text); font-weight: 700; }
.brand:hover { text-decoration: none; }
.brand-mark {
  width: 22px; height: 22px;
  display: inline-block;
  flex-shrink: 0;
}
.site-main { min-height: 60vh; }

.site-footer { border-top: 1px solid var(--color-border); margin-top: 3rem; }
.site-footer-inner { display: block; color: var(--color-text-muted); font-size: 0.9rem; }

.hero { padding: 4rem 1.25rem; }
.hero-inner { max-width: 720px; margin: 0 auto; text-align: center; }
.hero h1 { font-size: 2.5rem; margin-bottom: 1rem; }
.hero-sub { font-size: 1.2rem; color: var(--color-text-muted); margin-bottom: 1.75rem; }

/* The badge above the headline. It wears `.pill`'s shape, but on the home page
   it is an `<a>` to the waitlist rather than a status flag in a table cell --
   so it is a WCAG 2.5.8 target and has to clear 24px, where `.pill`'s 0.1rem
   of vertical padding left it 21.2 tall. The padding is overridden here and
   not on `.pill`, because every one of the product's three dozen other pills
   is a `<span>` in a row and must not grow with it; the compound selector is
   what wins against `.pill` from further down the file. The wider side padding
   keeps the text clear of the 999px radius at the taller height, and
   `min-height` states the floor so a later font-size change cannot drop back
   under it. The waitlist page renders the same badge as a `<span>` -- not a
   target, but it carries this class too, so the two hero pages do not disagree
   by 6px over the same component. Both templates used to carry the margin and
   the display as inline styles. */
.pill.hero-badge { min-height: 24px; margin-bottom: 1rem; padding: 0.3rem 0.85rem; }

/* Hero typewriter rotator */
.hero-rotator .hero-line { display: block; }
.typewriter { color: var(--color-primary); white-space: nowrap; }
.typewriter-word { font-style: italic; }
.typewriter-caret {
  display: inline-block;
  width: 0.06em;
  height: 1em;
  margin-left: 0.06em;
  vertical-align: -0.12em;
  background-color: currentColor;
  opacity: 0;
}
.typewriter.is-animating .typewriter-caret {
  opacity: 1;
  animation: typewriter-blink 1s step-end infinite;
}
@keyframes typewriter-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .typewriter-caret { display: none; }
}
.hero-actions { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; }
.hero-note { margin-top: 1rem; color: var(--color-text-muted); font-size: 0.9rem; }

/* ----------------------------------------------------------- Auth screens */
.auth-page {
  min-height: 70vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1.25rem;
}
.auth-card { width: 100%; max-width: 420px; }
.auth-card h1 { margin-bottom: 0.5rem; }
.auth-card .muted { margin-bottom: 1.25rem; }
.auth-alt { margin-top: 1rem; font-size: 0.9rem; color: var(--color-text-muted); }

.studio-list { list-style: none; display: flex; flex-direction: column; gap: 0.5rem; margin-top: 1rem; }
.studio-list-item {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0.9rem 1rem; border: 1px solid var(--color-border);
  border-radius: var(--radius-sm); color: var(--color-text);
}
.studio-list-item:hover { background: var(--color-surface-hover); text-decoration: none; }
.studio-list-role { color: var(--color-text-muted); font-size: 0.85rem; }

/* ------------------------------------------------------------- App shell */
.app-body { background: var(--color-bg); }
.app-shell { display: flex; min-height: 100vh; }
.app-sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: var(--color-surface);
  border-right: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  padding: 1rem;
  gap: 1rem;
}
.app-sidebar-head { padding: 0.25rem 0.25rem 0.75rem; border-bottom: 1px solid var(--color-border); }
.app-nav { display: flex; flex-direction: column; gap: 0.25rem; flex: 1; }
.app-nav-link {
  padding: 0.55rem 0.75rem; border-radius: var(--radius-sm);
  color: var(--color-text); font-weight: 500;
}
.app-nav-link:hover { background: var(--color-surface-hover); text-decoration: none; }
/* The link for the page you are on. Keyed off the attribute a reader hears
   rather than a class, so the announced state and the painted one cannot
   drift apart. Three signals, not one: the fill, the weight and the edge bar,
   because color alone fails anyone who cannot tell these two blues apart
   (WCAG 1.4.1). The bar is --color-primary-text rather than --color-primary --
   the brighter token measures 2.82:1 on dark --color-surface-2, under the 3:1
   a non-text indicator owes, while primary-text clears it in both schemes. The
   ink is set explicitly so .app-nav-upgrade's accent color does not survive
   onto this fill, where it would be unreadable in dark mode.
   Both pairs are asserted in tests/test_contrast.py. */
.app-nav-link[aria-current="page"] {
  background: var(--color-surface-2);
  color: var(--color-text);
  font-weight: 700;
  box-shadow: inset 3px 0 0 var(--color-primary-text);
}
/* The one nav link with an accent ink of its own, on the sidebar's
   --color-surface -- where --color-primary was 3.98:1 in dark mode, so it
   carries --color-primary-text for the reason `a` does: 6.41:1 / 5.75:1 (T64).
   `.app-nav-link:hover` tints it as well, and that tint is
   --color-surface-hover so this accent survives onto it readably at 4.72:1
   dark (T65); on --color-surface-2 it was 4.07:1. The current-page rule above
   still overrides the ink anyway, because that rule's fill *is*
   --color-surface-2 and no primary token reads at text size there. */
.app-nav-upgrade { color: var(--color-primary-text); }
.app-sidebar-foot { border-top: 1px solid var(--color-border); padding-top: 0.75rem; }

.app-content { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.app-topbar {
  display: flex; align-items: center; gap: 1rem;
  padding: 0.75rem 1.25rem;
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
  position: sticky; top: 0; z-index: 10;
}
.app-topbar-title { font-weight: 700; }
.app-topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 0.75rem; }
.app-user { color: var(--color-text-muted); font-size: 0.9rem; }
.app-main { padding: 1.5rem 1.25rem; flex: 1; }
.page { max-width: 960px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.25rem; }
.page-head h1 { margin-bottom: 0.25rem; }

.trial-badge {
  font-size: 0.8rem; font-weight: 600;
  padding: 0.25rem 0.6rem; border-radius: 999px;
  background: var(--color-surface-2); color: var(--color-text-muted);
}

/* The hamburger toggle. `padding: 0.25rem` was all the size it had, and the
   span it wraps is 2px tall -- the two outer bars are ::before/::after at
   `top: -7px` and `top: 7px`, absolutely positioned, so they draw a 22x16 mark
   and contribute nothing to this button's height. The box measured 30x10 at
   390px: a visitor saw the hamburger and could press a sliver of it, on the one
   control that reveals the whole navigation on a phone. The two `--tap-min`
   declarations are the target; the bars are untouched. Harmless above 820px,
   where `display: none` is what this rule is mostly for. */
.app-menu-btn {
  display: none; background: none; border: none; cursor: pointer;
  min-width: var(--tap-min); min-height: var(--tap-min);
  padding: 0.25rem;
}
/* The two pointer closers for the off-canvas menu, both off above the
   breakpoint for the same reason the toggle is: above it the sidebar is the
   always-visible left column, so a Close button would be a dead control and a
   backdrop would be a sheet over the whole page. The narrow-only halves are in
   the responsive block below, beside the panel they close. */
.app-menu-close, .app-menu-backdrop { display: none; }
/* `line-height` carries the height here rather than a flex centering, because
   the narrow rule below needs to stay `display: block` (what
   tests/test_sidebar_closers.py reads as "switched on below the breakpoint")
   and a block box takes its height from its line box. A 44px line box both
   reaches the target and centers the glyph in it; `min-height` says the same
   thing in the property the scan reads first. The glyph stays 1.5rem. */
.app-menu-close {
  background: none; border: none; cursor: pointer;
  font-size: 1.5rem; color: var(--color-text-muted);
  min-width: var(--tap-min); min-height: var(--tap-min);
  line-height: var(--tap-min); text-align: center;
}
.app-menu-bars, .app-menu-bars::before, .app-menu-bars::after {
  display: block; width: 22px; height: 2px; background: var(--color-text); position: relative;
}
.app-menu-bars::before, .app-menu-bars::after { content: ""; position: absolute; left: 0; }
.app-menu-bars::before { top: -7px; }
.app-menu-bars::after { top: 7px; }


/* ---------------------------------------------------------------- Toasts */
.toast-stack {
  position: fixed; top: 1rem; right: 1rem; z-index: 50;
  display: flex; flex-direction: column; gap: 0.5rem; max-width: 360px;
}
.toast {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.75rem 1rem; border-radius: var(--radius-sm);
  background: var(--color-surface); border: 1px solid var(--color-border);
  box-shadow: var(--shadow-lg); border-left-width: 4px;
}
.toast-body { flex: 1; font-size: 0.9rem; }
/* The dismiss glyph had no padding at all, so its box was the advance width of
   a single `&times;` -- about 10px across. Sized the same way .app-menu-close
   is, and the toast now centers its row rather than top-aligning it, so the
   glyph sits level with the message instead of above it. A one-line toast is
   taller by the difference; that is what a pressable dismiss costs. */
.toast-close {
  background: none; border: none; cursor: pointer;
  font-size: 1.1rem; color: var(--color-text-muted);
  min-width: var(--tap-min); min-height: var(--tap-min);
  line-height: var(--tap-min); text-align: center;
}
.toast-success { border-left-color: var(--color-success); }
.toast-error { border-left-color: var(--color-danger); }
.toast-warning { border-left-color: var(--color-warning); }
.toast-info, .toast-debug { border-left-color: var(--color-info); }

/* ---------------------------------------------------------------- Modals */
.modal-overlay {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(0, 0, 0, 0.5);
  display: flex; align-items: center; justify-content: center;
  padding: 1.25rem;
}
.modal {
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius); padding: 1.5rem; max-width: 460px; width: 100%;
  box-shadow: var(--shadow-lg);
}
.modal-message { margin-bottom: 1.25rem; }
.modal-actions { display: flex; justify-content: flex-end; gap: 0.6rem; }

/* ----------------------------------------------------------- Responsive */
@media (max-width: 820px) {
  /* `flex` rather than `block`: the base rule gives this button a 44px box and
     the bars are a 22x2 span inside it, so something has to center the mark in
     the target. The size itself is in the base rule, where it is inert because
     the button is `display: none` above here. */
  .app-menu-btn { display: flex; align-items: center; justify-content: center; }

  /* Off-canvas sidebar. `visibility` is the load-bearing declaration here, not
     the transform: a transform moves an element, it does not take it out of the
     tab order or the accessibility tree. With the translate alone, a keyboard
     visitor on a phone-width viewport tabbed through all 21 invisible nav links
     before reaching anything on screen -- the complaint the skip link answers,
     reappearing off-screen where the skip link cannot help, because the sidebar
     is not in the preamble it jumps over.

     This is the CSS answer rather than `:inert="!sidebarOpen"` on the <aside>
     deliberately. The same markup is the always-visible sidebar above 820px, so
     an attribute bound to `sidebarOpen` alone would make the desktop sidebar
     unreachable, and keeping it narrow-only would mean a second copy of this
     breakpoint in JavaScript, watched by matchMedia, that could drift from this
     one. Here the breakpoint is stated once, in the language that owns it, and
     `visibility: hidden` removes the links from both the tab order and the
     accessibility tree for exactly as long as the layout is off-canvas.

     `visibility` is deliberately NOT in the transition list: delaying the hide
     so the slide-out could play would leave the links tabbable for the length
     of the animation. An instant close is a fair price for a menu that is never
     a phantom tab stop; opening still slides, since `visible` applies at once
     and the transform animates under it. */
  .app-sidebar {
    position: fixed; inset: 0 auto 0 0; z-index: 40;
    transform: translateX(-100%); visibility: hidden;
    transition: transform 0.2s ease;
  }
  .app-sidebar.is-open { transform: translateX(0); visibility: visible; box-shadow: var(--shadow-lg); }

  /* The open panel is fixed at the left edge and `var(--sidebar-w)` wide, so it
     sits on top of .app-menu-btn -- the control that closes it. A keyboard got
     out anyway (Tab out of the menu reaches the toggle, Enter there closes it),
     which is why this outlived the keyboard work; a pointer had nothing to
     press. These two are that way out.

     Both are hidden the same way the panel is, with `visibility` rather than
     `transform` alone, and for the same reason: `visibility: hidden` is what
     takes a thing out of the tab order and the accessibility tree, and -- the
     half that matters for the backdrop -- out of hit testing, so the closed
     backdrop cannot swallow a click meant for the page. The `display: none`
     above this query is what keeps either from existing at desktop width. */
  /* The padding this used to carry was its whole size (0.1rem 0.4rem around a
     glyph); `--tap-min` in the base rule replaces it, so it is gone rather than
     added to. */
  .app-menu-close { display: block; margin-left: auto; }
  .app-sidebar-head { display: flex; align-items: center; gap: 0.5rem; }
  .app-menu-backdrop {
    display: block; position: fixed; inset: 0; z-index: 39;
    background: rgba(0, 0, 0, 0.45);
    visibility: hidden; opacity: 0;
    transition: opacity 0.2s ease;
  }
  .app-menu-backdrop.is-open { visibility: visible; opacity: 1; }
  .hero h1 { font-size: 2rem; }
}

/* ============================================================ Marketing */
.container { max-width: var(--maxw); margin: 0 auto; padding: 0 1.25rem; }
.container.narrow { max-width: 760px; }
.container-md { max-width: 820px; margin: 0 auto; padding: 0 1.25rem; }

.marketing-section { padding: 3.5rem 0; }
.marketing-section.alt { background: var(--color-surface); }
.section-head { text-align: center; max-width: 680px; margin: 0 auto 2.25rem; }
.section-head h1 { font-size: 2.25rem; margin-bottom: 0.6rem; }
.section-head h2 { font-size: 1.75rem; margin-bottom: 0.5rem; }
.section-cta { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; margin-top: 2rem; }
.eyebrow { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; font-weight: 600; }

/* Stats band */
.stats-band { background: var(--color-surface); padding: 2.25rem 0; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1.5rem; text-align: center; }
.stat { display: flex; flex-direction: column; gap: 0.25rem; }
.stat-num { font-size: 1.9rem; font-weight: 800; color: var(--color-primary); }
.stat-cap { font-size: 0.9rem; }

/* Problem + feature grids */
.problem-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.25rem; }
.problem-card h3 { margin-bottom: 0.5rem; }
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.25rem; }
.feature-card h3 { margin-bottom: 0.4rem; }

.feature-group { margin-bottom: 2rem; }
.feature-group h2 { margin-bottom: 0.75rem; border-bottom: 1px solid var(--color-border); padding-bottom: 0.4rem; }

.check-list { list-style: none; display: grid; gap: 0.6rem; }
.check-list li { position: relative; padding-left: 1.6rem; }
.check-list li::before {
  content: ""; position: absolute; left: 0; top: 0.45em;
  width: 0.7rem; height: 0.4rem; border-left: 2px solid var(--color-success);
  border-bottom: 2px solid var(--color-success); transform: rotate(-45deg);
}

/* CTA band */
.cta-band { text-align: center; background: var(--color-surface); }
.cta-band h2 { font-size: 1.9rem; margin-bottom: 0.5rem; }
.cta-band .hero-actions { margin-top: 1.5rem; }

/* Waitlist inline form */
.waitlist { margin-top: 1.75rem; }
.waitlist-lead { margin-bottom: 0.6rem; font-size: 0.9rem; }
.waitlist-row { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; }
.waitlist-row input[type="email"] { max-width: 320px; }
.form-success-inline { color: var(--color-success-text); font-weight: 600; }

/* Pricing */
.billing-toggle {
  display: inline-flex; gap: 0.25rem; margin: 0 auto 2rem; padding: 0.25rem;
  background: var(--color-surface-2); border-radius: 999px;
  position: relative; left: 50%; transform: translateX(-50%);
}
.toggle-opt {
  border: none; background: transparent; cursor: pointer; font: inherit;
  padding: 0.5rem 1.1rem; border-radius: 999px; color: var(--color-text-muted);
  display: inline-flex; align-items: center; gap: 0.4rem;
}
.toggle-opt.active { background: var(--color-bg); color: var(--color-text); box-shadow: var(--shadow); }
/* The one place in the product where a focus ring is painted on
   --color-surface-2, and the one surface --color-primary cannot be seen on:
   the group's 0.25rem of padding is 4px of that fill all the way around both
   buttons, so the shell's ring at a 2px offset lands inside it on every side,
   where --color-primary measures 2.82:1 in dark mode against WCAG 1.4.11's
   3:1 (4.70:1 in light, so the defect was dark-mode-only).
   --color-primary-text instead, which is 6.09:1 / 4.07:1 on the same fill --
   the same substitution, for the same reason, that the current nav link's
   inset edge bar makes 230 lines above. Not a new focus color: it is the
   token this design system already nominates for an indicator on this
   surface. The padding stays, because it is what makes the active pill read
   as a pill -- its --color-bg face is only 1.10:1 (light) / 1.72:1 (dark)
   against the track, so the 4px frame is the whole of the distinction. On
   `.toggle-opt`, not `.toggle-opt.active`: the inactive button has no fill of
   its own (measured `rgba(0, 0, 0, 0)`), so its band is the track's too and
   an inset ring would only move half the defect. */
.billing-toggle .toggle-opt:focus-visible { outline-color: var(--color-primary-text); }
.toggle-badge { font-size: 0.7rem; color: var(--color-success-text); font-weight: 700; }

.pricing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; max-width: 760px; margin: 0 auto; }
.pricing-card { position: relative; display: flex; flex-direction: column; gap: 0.75rem; }
.pricing-card.is-highlight { border-color: var(--color-primary); box-shadow: 0 0 0 1px var(--color-primary), var(--shadow); }
.pricing-flag {
  position: absolute; top: -0.7rem; right: 1rem; background: var(--color-primary-fill);
  color: var(--color-on-fill); font-size: 0.72rem; font-weight: 700; padding: 0.2rem 0.6rem; border-radius: 999px;
}
.pricing-blurb { min-height: 2.6em; }
.pricing-amount { display: flex; align-items: baseline; gap: 0.1rem; }
.price-currency { font-size: 1.3rem; font-weight: 700; }
.price-value { font-size: 2.6rem; font-weight: 800; }
.price-period { color: var(--color-text-muted); font-weight: 600; }
.price-foot { font-size: 0.85rem; margin-top: -0.4rem; }

.promise { max-width: 760px; margin: 2.25rem auto 0; border-left: 4px solid var(--color-success); }
.promise h3 { margin-bottom: 0.4rem; }

.sms-packs { max-width: 760px; margin: 2.5rem auto 0; text-align: center; }
.sms-packs h2 { margin-bottom: 0.4rem; }
.sms-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; margin-top: 1.25rem; }
.sms-card { display: flex; flex-direction: column; gap: 0.3rem; align-items: center; }
.sms-name { font-weight: 700; }
.sms-price { font-size: 1.4rem; font-weight: 800; }
.sms-texts { font-size: 0.85rem; }

/* FAQ */
.faq-list { display: flex; flex-direction: column; gap: 0.75rem; }
.faq-item { border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: 0.9rem 1.1rem; background: var(--color-bg); }
.faq-item summary { cursor: pointer; font-weight: 600; list-style: none; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item[open] summary { margin-bottom: 0.6rem; }

/* Forms / contact */
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.form-success { text-align: center; display: flex; flex-direction: column; gap: 0.75rem; align-items: center; }

/* Prose / legal */
.prose { display: flex; flex-direction: column; gap: 0.5rem; }
.prose h2 { font-size: 1.25rem; margin-top: 1.5rem; }
.prose p, .prose ul { margin-bottom: 0.5rem; }
.prose ul { padding-left: 1.25rem; display: flex; flex-direction: column; gap: 0.3rem; }
.legal h1 { margin-bottom: 0.25rem; }
.legal .updated { margin-bottom: 1.5rem; }

/* Footer links */
.site-footer-links { display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; margin-bottom: 1rem; }
/* Nine links in a row, each one a target in its own right. Bare `<a>` text at
   this size is a 21.6px line box, under WCAG 2.5.8's 24x24 floor, and the
   "in a sentence" exemption does not cover them: the only other text beside
   one of these links is another link, and that exception is about non-target
   text. The padding is vertical only -- every one of the nine is already wider
   than the floor (27.9px at the narrowest, "FAQ"), and horizontal padding
   would indent the first link out of line with the paragraph below it. The
   row's 0.75rem row-gap is left alone, so the boxes still clear each other by
   12px and the footer still reads as a row rather than a stack of blocks. */
.site-footer-links a {
  color: var(--color-text-muted);
  font-size: 0.9rem;
  padding: 0.25rem 0;
}
/* The current page in the footer nav. These start muted, so the mark can
   afford to be the full text ink as well as the weight. */
.site-footer-links a[aria-current="page"] {
  color: var(--color-text);
  font-weight: 700;
}

.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;
}

/* Skip link: the first focusable thing in every page document, so a keyboard
   visitor can jump straight to the content instead of tabbing the sidebar and
   topbar again on every page. Hidden by the `.sr-only` technique until it is
   focused, then brought on screen above the sticky navs (z-index 20 is the
   highest of those).

   `:focus`, not `:focus-visible`: this one must appear for every way of
   reaching it. A skip link that stayed hidden while holding focus would be a
   keyboard trap wearing an invisibility cloak. */
.skip-link {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
.skip-link:focus {
  width: auto; height: auto; clip: auto; overflow: visible;
  top: 0.5rem; left: 0.5rem; z-index: 100;
  padding: 0.6rem 1rem;
  background: var(--color-surface);
  color: var(--color-text);
  border: 2px solid var(--color-primary);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  text-decoration: none;
}

@media (max-width: 600px) {
  .form-row { grid-template-columns: 1fr; }
  .section-head h1 { font-size: 1.75rem; }
}

/* ============================================================ Billing */
.banner {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  flex-wrap: wrap; padding: 0.85rem 1.1rem; border-radius: var(--radius-sm);
  margin-bottom: 1.25rem; border: 1px solid var(--color-border);
}
/* The bar is --color-warning-text rather than --color-warning because the wash
   on its right is 14% of that same hue: the bar was 2.75:1 against its own
   fill in light mode, under 1.4.11's 3:1, while clearing it against the page
   on its left. The *-text token reads 4.96:1 inside and 5.76:1 outside, and is
   the identical hex in dark mode, so only light mode moves. T67. */
.banner-warning { background: color-mix(in srgb, var(--color-warning) 14%, var(--color-bg)); border-left: 4px solid var(--color-warning-text); }
.btn-sm { padding: 0.35rem 0.7rem; font-size: 0.85rem; }
.current-plan-badge {
  display: inline-block; padding: 0.4rem 0.8rem; border-radius: var(--radius-sm);
  background: var(--color-surface-2); color: var(--color-text-muted); font-weight: 600;
  text-align: center;
}

/* ===================================================== Vertical settings */
.settings-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 1.25rem; align-items: start; }
.vertical-preview { display: flex; flex-direction: column; gap: 1.25rem; }
.preview-block h3 { margin-bottom: 0.6rem; }
.preview-terms { list-style: none; display: flex; flex-direction: column; gap: 0.35rem; }
.preview-terms li { display: flex; justify-content: space-between; gap: 1rem; }
.preview-features { list-style: none; display: flex; flex-direction: column; gap: 0.4rem; }
.preview-features li { display: flex; align-items: center; gap: 0.5rem; }
.feat-dot { width: 0.6rem; height: 0.6rem; border-radius: 999px; background: var(--color-border); display: inline-block; }
.preview-features li.is-on .feat-dot { background: var(--color-success); }
.preview-features li.is-off { color: var(--color-text-muted); }
.small { font-size: 0.85rem; }
@media (max-width: 820px) { .settings-grid { grid-template-columns: 1fr; } }

/* ============================================================ Families CRM */
.list-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.head-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.narrow-page { max-width: 680px; }
.list-filters { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.25rem; }
.list-filters input[type="search"] { flex: 1; min-width: 200px; }
.list-filters select { max-width: 180px; }

.table-card { padding: 0; overflow: hidden; }
.data-table { width: 100%; border-collapse: collapse; }
.data-table th, .data-table td { text-align: left; padding: 0.75rem 1rem; border-bottom: 1px solid var(--color-border); }
.data-table th { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--color-text-muted); }
.data-table tbody tr:last-child td { border-bottom: none; }
/* The row is not itself a click target -- the link in its first cell is, and
   that is the one affordance a keyboard and a reader can reach.  So no
   `cursor: pointer` here: it would promise a whole-row click only a mouse
   could collect.  The hover tint stays; it reads as "this is one row". */
.clickable-row:hover { background: var(--color-surface-hover); }
/* A row is 0.75rem/1rem of cell padding deep all the way around the link in
   its first cell -- so when a pointer is resting on the row while the keyboard
   focuses that link, the shell's ring at a 2px offset lands on the hover tint
   on every side. Hover and :focus-visible co-occur routinely: the pointer
   stays wherever it was left while the keyboard walks. When that tint was
   --color-surface-2, --color-primary measured 4.70:1 (light) and 2.82:1 (dark)
   against WCAG 1.4.11's 3:1, so the ring took --color-primary-text instead
   (T56's substitution on the pricing toggle, and the current nav link's inset
   bar above) -- 6.09:1 / 4.07:1.
   T65 moved the tint itself to --color-surface-hover, for the link's *ink*
   rather than for its ring, and that changes what this line is doing: the ring
   is now 6.09:1 / 4.72:1 and the --color-primary it replaces would be 3.26:1
   dark, which clears 3:1. It had to -- the two inks differ by a fixed
   luminance factor, so a fill where --color-primary-text reads at 4.5:1 is
   always one where --color-primary clears 3:1 (4.5 / 1.4458 = 3.11). So this
   override is no longer the difference between a visible ring and an invisible
   one; it is kept because it is the ink the product already uses on a tinted
   fill and it keeps 1.4x the margin. The tint stays either way: it is what
   makes the row read as one row.
   Measured with the pointer parked in tests/e2e/test_color.py. */
.clickable-row:hover a:focus-visible { outline-color: var(--color-primary-text); }
.clickable-row a { position: relative; z-index: 1; }
.empty-row { text-align: center; padding: 2rem 1rem; }

.pill { display: inline-block; padding: 0.1rem 0.55rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; background: var(--color-surface-2); color: var(--color-text-muted); }
.pill-active { background: color-mix(in srgb, var(--color-success) 18%, var(--color-bg)); color: var(--color-success-text); }
.pill-inactive, .pill-withdrawn { background: var(--color-surface-2); color: var(--color-text-muted); }

.detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.25rem; align-items: start; }
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; }
.row-item { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid var(--color-border); }
.row-item:last-child { border-bottom: none; }

.flag-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; }
.flag { display: inline-block; padding: 0.15rem 0.6rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
.flag-red { background: color-mix(in srgb, var(--color-danger) 18%, var(--color-bg)); color: var(--color-danger-text); }
.flag-amber { background: color-mix(in srgb, var(--color-warning) 18%, var(--color-bg)); color: var(--color-warning-text); }
.flag-green { background: color-mix(in srgb, var(--color-success) 18%, var(--color-bg)); color: var(--color-success-text); }
.flag-blue { background: color-mix(in srgb, var(--color-primary) 18%, var(--color-bg)); color: var(--color-primary-text); }
.flag-gray { background: var(--color-surface-2); color: var(--color-text-muted); }
.flag-add-form { display: flex; gap: 0.35rem; align-items: center; }
.flag-add-form input { max-width: 140px; }

.note-form { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1rem; }
.note-list { list-style: none; display: flex; flex-direction: column; gap: 0.6rem; }
.note-list li { padding: 0.6rem 0.75rem; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius-sm); }
.note-list li.pinned { border-left: 3px solid var(--color-primary); }
.form-actions { display: flex; gap: 0.6rem; margin-top: 0.5rem; }
.checkbox-group { flex-direction: row; align-items: center; gap: 0.5rem; }
.checkbox-group input { width: auto; }

/* Parent portal */
.portal-body { background: var(--color-bg); }
.portal-topbar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--color-border); background: var(--color-surface); }
.portal-topbar-actions { display: flex; align-items: center; gap: 0.75rem; }
.portal-main { padding: 1.5rem 1.25rem; }
.portal-page { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.25rem; }
.portal-students { list-style: none; display: flex; flex-direction: column; gap: 0.4rem; margin: 0.5rem 0; }

/* ============================================================ Classes */
.wide-page { max-width: 1100px; }
.simple-list { list-style: none; display: flex; flex-direction: column; gap: 0.5rem; }
.simple-list li { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0; border-bottom: 1px solid var(--color-border); }
.simple-list li:last-child { border-bottom: none; }
.swatch { width: 0.9rem; height: 0.9rem; border-radius: 4px; display: inline-block; }
.form-error { color: var(--color-danger-text); background: color-mix(in srgb, var(--color-danger) 10%, var(--color-bg)); border: 1px solid var(--color-danger); border-radius: var(--radius-sm); padding: 0.6rem 0.8rem; }
.meeting-list { list-style: none; display: flex; flex-direction: column; gap: 0.3rem; max-height: 320px; overflow-y: auto; }
.meeting-list li.canceled { text-decoration: line-through; color: var(--color-text-muted); }

.cal-controls { display: flex; gap: 0.5rem; }
.cal-scroll { overflow-x: auto; padding: 0; }
.cal-grid { width: 100%; border-collapse: collapse; min-width: 760px; }
.cal-grid th, .cal-grid td { border: 1px solid var(--color-border); vertical-align: top; padding: 0.4rem; }
.cal-grid thead th { background: var(--color-surface-2); font-size: 0.8rem; text-align: center; padding: 0.5rem; }
.cal-rowhead { background: var(--color-surface-2); font-size: 0.85rem; white-space: nowrap; min-width: 120px; }
.cal-cell { width: 13%; height: 100%; }
.cal-class { display: flex; flex-direction: column; gap: 0.05rem; padding: 0.35rem 0.5rem; margin-bottom: 0.35rem; background: var(--color-bg); border: 1px solid var(--color-border); border-left: 3px solid var(--color-primary); border-radius: var(--radius-sm); color: var(--color-text); }
.cal-class:hover { background: var(--color-surface-hover); text-decoration: none; }
.cal-time { font-size: 0.72rem; color: var(--color-text-muted); }
.cal-name { font-weight: 600; font-size: 0.85rem; }
.cal-meta { font-size: 0.72rem; }

/* ============================================================ Enrollment */
.add-student-form { margin-bottom: 1.25rem; }
.add-row { display: flex; gap: 0.5rem; margin-top: 0.4rem; }
.add-row select { flex: 1; }
.class-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
.class-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.class-card-head h2 { font-size: 1.1rem; }
.enroll-row { display: flex; gap: 0.5rem; margin-top: 0.6rem; }
.enroll-row select { flex: 1; }
.embed-body { padding: 0.5rem; background: transparent; }
.embed-table { width: 100%; }

/* ============================================================ Attendance */
.meeting-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; }
.meeting-card { display: flex; gap: 0.9rem; align-items: center; color: var(--color-text); }
.meeting-card:hover { background: var(--color-surface-hover); text-decoration: none; }
/* 1.1rem/700 is 17.6px bold, under WCAG 1.4.3's 18.66px large-bold threshold,
   so the 3:1 allowance `.stat-num` earns does not cover this -- and the card
   it labels is `a.card.meeting-card`, i.e. --color-surface, where
   --color-primary was 3.98:1 in dark mode. --color-primary-text is 6.41:1 /
   5.75:1 there (T64), and 6.09:1 / 4.72:1 on the --color-surface-hover the
   card's own :hover rule tints it -- which is the fill T65 added so this ink
   survives the pointer; on --color-surface-2 it read 4.07:1. */
.meeting-card-time { font-weight: 700; font-size: 1.1rem; color: var(--color-primary-text); min-width: 4.5rem; }
.meeting-card-body { display: flex; flex-direction: column; }

.att-roster { display: flex; flex-direction: column; }
.att-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.6rem 1rem; border-bottom: 1px solid var(--color-border); }
.att-row:last-child { border-bottom: none; }
.att-name { font-weight: 500; }
.att-buttons { display: flex; gap: 0.3rem; }
.att-btn { width: 2.4rem; height: 2.4rem; border-radius: var(--radius-sm); border: 1px solid var(--color-border-strong); background: var(--color-bg); color: var(--color-text-muted); font-weight: 700; cursor: pointer; }
.att-btn:hover { border-color: var(--color-primary); }
.att-present.is-active { background: var(--color-success-fill); color: var(--color-on-fill); border-color: var(--color-success-fill); }
.att-absent.is-active { background: var(--color-danger-fill); color: var(--color-on-fill); border-color: var(--color-danger-fill); }
.att-late.is-active { background: var(--color-warning-fill); color: var(--color-on-fill); border-color: var(--color-warning-fill); }
.att-excused.is-active { background: var(--color-info-fill); color: var(--color-on-fill); border-color: var(--color-info-fill); }
/* The non-color half of "this is the status that is marked" (T71): a shape
   beside the letter, small enough that the pair still fits the button's fixed
   2.4rem, so the four never move when one is marked. */
.att-mark { margin-left: 0.15rem; font-size: 0.8rem; }

.kiosk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 0.75rem; }
.kiosk-tile { min-height: 90px; border-radius: var(--radius); border: 1px solid var(--color-border-strong); background: var(--color-surface); color: var(--color-text); font-size: 1.05rem; font-weight: 600; cursor: pointer; padding: 0.75rem; }
.kiosk-tile:hover { border-color: var(--color-primary); }
.kiosk-tile.is-done { background: var(--color-success-fill); color: var(--color-on-fill); border-color: var(--color-success-fill); cursor: default; }
.kiosk-check { font-size: 0.8rem; font-weight: 500; }
.clock-card { display: flex; flex-direction: column; gap: 0.75rem; align-items: flex-start; }

/* ============================================================ Billing */
.inline-pay { display: flex; gap: 0.3rem; align-items: center; }
.balance-tag { font-size: 0.95rem; }
.pill-paid { background: color-mix(in srgb, var(--color-success) 18%, var(--color-bg)); color: var(--color-success-text); }
.pill-open { background: color-mix(in srgb, var(--color-primary) 16%, var(--color-bg)); color: var(--color-primary-text); }
.pill-past_due, .pill-uncollectible { background: color-mix(in srgb, var(--color-danger) 16%, var(--color-bg)); color: var(--color-danger-text); }
.pill-draft, .pill-void { background: var(--color-surface-2); color: var(--color-text-muted); }

/* ============================================================ Messaging */
.pill-sent { background: color-mix(in srgb, var(--color-success) 18%, var(--color-bg)); color: var(--color-success-text); }
.pill-failed { background: color-mix(in srgb, var(--color-danger) 16%, var(--color-bg)); color: var(--color-danger-text); }
.pill-blocked_quota, .pill-skipped_optout { background: var(--color-surface-2); color: var(--color-text-muted); }
.thread { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; max-height: 420px; overflow-y: auto; }
.bubble { max-width: 80%; padding: 0.5rem 0.8rem; border-radius: var(--radius); }
.bubble-in { align-self: flex-start; background: var(--color-surface-2); }
.bubble-out { align-self: flex-end; background: var(--color-primary-fill); color: var(--color-on-fill); }
.bubble-out .muted { color: var(--color-on-fill); font-size: 0.75rem; }
.reply-form { display: flex; gap: 0.5rem; }
.reply-form textarea { flex: 1; }

/* ============================================================ Recitals */
.lineup { margin: 0; padding-left: 1.5rem; display: flex; flex-direction: column; gap: 0.35rem; }
.pill-planning { background: var(--color-surface-2); color: var(--color-text-muted); }
.pill-published { background: color-mix(in srgb, var(--color-success) 18%, var(--color-bg)); color: var(--color-success-text); }
.pill-completed { background: color-mix(in srgb, var(--color-primary) 16%, var(--color-bg)); color: var(--color-primary-text); }
.ticket-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.5rem 0; border-bottom: 1px solid var(--color-border); }

/* --- KPI cards (Slice 14 reports) --- */
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.kpi { display: flex; flex-direction: column; gap: 0.35rem; }
.kpi-label { font-size: 0.85rem; color: var(--color-text-muted); }
.kpi-value { font-size: 1.6rem; font-weight: 700; line-height: 1.1; }

/* --- Leads pipeline board (Slice 15) --- */
.pipeline-board { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(180px, 1fr); gap: 1rem; overflow-x: auto; padding-bottom: 0.5rem; }
.pipeline-col { min-width: 180px; }
.pipeline-col-head { font-size: 0.95rem; margin-bottom: 0.5rem; }

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