@charset "UTF-8";
/* Design tokens: kek.academy palette (kid-friendly, calm).
   Canonical source for all 6 frontends (host, adminPanel, city, trainings, learning,
   daily-activity) — each app's own src/styles/variables.{css,scss} is now just a one-line
   import of this file. Previously this content was hand-copied into each app and had already
   drifted to byte-level differences only in comment wording (centralized 2026-10-20, no value
   changes). adminPanel doesn't use the per-child site themes below (notebook/blackboard/boy/
   girl) — it still imports this whole file for simplicity; the unused theme selectors are inert
   there since adminPanel never sets data-theme on <html>. */
:root {
  --color-primary: #3fa9e0;
  --color-primary-dark: #2b8ac2;
  --color-primary-light: #eaf6fd;

  --color-secondary: #4ecdc4;
  --color-secondary-dark: #2f9a92;
  --color-secondary-light: #e7faf8;

  --color-accent: #ffa552;
  --color-accent-dark: #f2883a;
  --color-accent-light: #fff3e6;

  --color-danger: #ff6b6b;
  --color-danger-light: #ffecec;

  --bg: #f6fafd;
  --surface: #ffffff;
  --text: #33475b;
  --text-h: #1d2b3a;
  --text-muted: #7c93ac;
  --border: #e3ecf3;

  /* Two-layer (tight contact + soft ambient) shadows read as "floating", not just dark —
     2026-10-20 tactility pass. --shadow-press is a flattened shadow for the :active/pressed
     state of clickable cards/buttons, so a click visibly "pushes" the element down. */
  --shadow-sm: 0 1px 2px rgba(51, 71, 91, 0.06), 0 1px 1px rgba(51, 71, 91, 0.04);
  --shadow-md: 0 2px 4px rgba(51, 71, 91, 0.05), 0 8px 16px rgba(51, 71, 91, 0.08);
  --shadow-lg: 0 4px 8px rgba(51, 71, 91, 0.05), 0 16px 32px rgba(51, 71, 91, 0.12);
  --shadow-press: 0 1px 2px rgba(51, 71, 91, 0.08);

  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 20px;
  --radius-full: 999px;

  --transition: 0.18s ease;

  --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
}
/* :not([data-theme]) / [data-theme="default"] scoping keeps a custom theme (notebook,
   blackboard, boy, girl) winning over OS dark mode instead of fighting it at equal specificity. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]),
  :root[data-theme="default"] {
    --color-primary: #5cc0f2;
    --color-primary-dark: #3fa9e0;
    --color-primary-light: rgba(63, 169, 224, 0.16);

    --color-secondary: #5fe0d6;
    --color-secondary-dark: #4ecdc4;
    --color-secondary-light: rgba(78, 205, 196, 0.16);

    --color-accent: #ffb670;
    --color-accent-dark: #ffa552;
    --color-accent-light: rgba(255, 165, 82, 0.16);

    --color-danger: #ff8787;
    --color-danger-light: rgba(255, 107, 107, 0.16);

    --bg: #131a20;
    --surface: #1b242c;
    --text: #b8c6d4;
    --text-h: #f1f5f8;
    --text-muted: #7c93ac;
    --border: #2a3540;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 1px rgba(0, 0, 0, 0.2);
    --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.3), 0 8px 16px rgba(0, 0, 0, 0.35);
    --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.3), 0 16px 32px rgba(0, 0, 0, 0.45);
    --shadow-press: 0 1px 2px rgba(0, 0, 0, 0.35);
  }
}
/* Site themes: fixed look each (no light/dark variant), selected per-child and stored in the
   DB (see Layout in host's App.tsx, which sets data-theme on <html>). --color-danger is
   intentionally left un-overridden everywhere — a destructive action should read the same in
   every theme. */
:root[data-theme="notebook"] {
  --color-primary: #2f4374;
  --color-primary-dark: #1f2e52;
  --color-primary-light: #e7ebf5;

  --color-secondary: #5b7a56;
  --color-secondary-dark: #42603e;
  --color-secondary-light: #e9f0e6;

  --color-accent: #b2453f;
  --color-accent-dark: #8a3530;
  --color-accent-light: #f7e6e4;

  --bg: #f6efdf;
  --surface: #fbf6ea;
  --text: #3a3226;
  --text-h: #241f16;
  --text-muted: #7a705c;
  --border: #d8cdb0;
}
:root[data-theme="blackboard"] {
  --color-primary: #8fd7ff;
  --color-primary-dark: #5cc0f2;
  --color-primary-light: rgba(143, 215, 255, 0.16);

  --color-secondary: #ffe37a;
  --color-secondary-dark: #f2cf4a;
  --color-secondary-light: rgba(255, 227, 122, 0.16);

  --color-accent: #ff9f7a;
  --color-accent-dark: #f2814f;
  --color-accent-light: rgba(255, 159, 122, 0.16);

  --bg: #1b1b19;
  --surface: #242422;
  --text: #e5e5e0;
  --text-h: #ffffff;
  --text-muted: #a8a8a0;
  --border: #3a3a35;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 1px rgba(0, 0, 0, 0.2);
  --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.3), 0 8px 16px rgba(0, 0, 0, 0.35);
  --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.3), 0 16px 32px rgba(0, 0, 0, 0.45);
  --shadow-press: 0 1px 2px rgba(0, 0, 0, 0.35);
}
:root[data-theme="boy"] {
  --color-primary: #2f8fd6;
  --color-primary-dark: #1f6fae;
  --color-primary-light: #e3f2fc;

  --color-secondary: #3fc9c1;
  --color-secondary-dark: #28a49c;
  --color-secondary-light: #e1f8f6;

  --color-accent: #ff7a54;
  --color-accent-dark: #e85c37;
  --color-accent-light: #ffe6df;

  --bg: #eef7fb;
  --surface: #ffffff;
  --text: #33475b;
  --text-h: #1d2b3a;
  --text-muted: #7c93ac;
  --border: #dcebf4;
}
:root[data-theme="girl"] {
  --color-primary: #ef8fb0;
  --color-primary-dark: #d96a91;
  --color-primary-light: #fdeef3;

  --color-secondary: #6bc9c3;
  --color-secondary-dark: #48a49d;
  --color-secondary-light: #e6f7f5;

  --color-accent: #ffb870;
  --color-accent-dark: #f29a45;
  --color-accent-light: #fff2e2;

  --bg: #fdf3f6;
  --surface: #ffffff;
  --text: #5b4152;
  --text-h: #3a2734;
  --text-muted: #9a7f8c;
  --border: #f3dbe4;
}
/* host-specific component classes. Shared atoms (buttons, cards, badges, forms, app chrome
   base, sidebar base, tooltip, modal, misc utilities) now live in
   ../../../shared-styles/components.css — see that file's header comment for why every app
   (not just host) imports it. Everything below is host-only: the header action buttons/
   burger/sidebar-toggle, the logout button, the theme-loading spinner, the richer
   UserSwitcher dropdown content (themes list, section labels, glow hint), the character
   companions + tic-tac-toe, and the per-theme .app-main background art. */
/* Shared component classes: buttons, cards, inputs, layout chrome.
   Canonical source for all 6 frontends (host, adminPanel, city, trainings, learning,
   daily-activity). Each one imports this file via a relative path at the top of its own
   src/styles/components.{css,scss} and keeps only its genuinely app-specific rules below
   that import.

   Why every app imports this (not just host, even for chrome like .sidebar/.app-header that
   only host actually renders): Module Federation gives each loaded remote's CSS bundle
   page-wide scope, with no style isolation — once any MFE's stylesheet is on the page, its
   selectors match host's DOM too. Previously every app hand-copied this file's rules locally,
   which is why they had already drifted by the time this got centralized (2026-10-20): most
   visibly, host's own .app-header omitted `justify-content: space-between` (it uses
   `.app-header__actions { margin-left: auto }` instead, see host's local file — intentional,
   per that rule's comment) while every other app's copy had added it back. Since host is the
   only app that actually renders #.app-header, host's version is the one that matters and is
   the canonical one below; the other copies' extra `justify-content` would have silently won
   the cascade on whichever env happened to load that MFE's bundle last. .app-main similarly
   picks up host's extra `position: relative` (needed for host's .theme-loading-overlay) —
   harmless everywhere else since nothing else positions a child against it. */
.page {
  max-width: 960px;
  margin: 0 auto;
  padding: 32px 24px 60px;
  text-align: left;
  box-sizing: border-box;
}
.page--narrow {
  max-width: 440px;
}
.page-title {
  display: flex;
  align-items: center;
  gap: 10px;
}
/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 20px;
  font-size: 15px;
  font-weight: 600;
  font-family: var(--sans);
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition:
    transform var(--transition),
    box-shadow var(--transition),
    background var(--transition),
    color var(--transition),
    opacity var(--transition);
}
.btn:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}
.btn:active:not(:disabled) {
  transform: translateY(1px);
  box-shadow: var(--shadow-press);
}
.btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}
.btn-primary {
  background: var(--color-primary);
  color: #fff;
}
.btn-primary:hover:not(:disabled) {
  background: var(--color-primary-dark);
}
.btn-secondary {
  background: var(--surface);
  color: var(--text);
  border: 1.5px solid var(--border);
  box-shadow: none;
}
.btn-secondary:hover:not(:disabled) {
  background: var(--color-primary-light);
  border-color: var(--color-primary);
  color: var(--color-primary-dark);
}
.btn-accent {
  background: var(--color-accent);
  color: #fff;
}
.btn-accent:hover:not(:disabled) {
  background: var(--color-accent-dark);
}
.btn-danger {
  background: var(--color-danger);
  color: #fff;
}
.btn-danger:hover:not(:disabled) {
  opacity: 0.9;
}
.btn-ghost {
  background: transparent;
  color: var(--color-primary);
  box-shadow: none;
  padding: 8px 12px;
}
.btn-ghost:hover:not(:disabled) {
  background: var(--color-primary-light);
  transform: none;
}
.btn-block {
  width: 100%;
}
.btn-sm {
  padding: 6px 14px;
  font-size: 13px;
}
/* Cards */
.card {
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: 20px 24px;
  text-align: left;
  transition:
    transform var(--transition),
    box-shadow var(--transition);
}
.card:hover {
  box-shadow: var(--shadow-lg);
}
/* Compact variant — example cards in the lesson room and schedule cards (LessonRoomPage /
   ScheduleDashboardPage in trainings/learning/daily-activity). Doesn't touch base .card. */
.card--compact {
  padding: 10px 14px;
}
.card-list .card--compact p {
  margin-bottom: 4px;
}
.card-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
/* Clickable "row" links styled as cards (classes/subjects/topics/subtopics lists, daily-
   activity hub) — this is where kids spend most of their selection-screen time, so it gets
   the most expressive tactile tier (vs. .sidebar__link/.btn's calmer tier): deeper hover
   lift, a squash on press, and cursor "magnetism" (2026-10-20, see MagneticLink in
   trainings/daily-activity — a no-op on touch, since there's no mousemove without a real
   pointer). --magnet-x/-y default to 0px so this is harmless wherever the hook isn't
   attached. */
.list-link-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 20px;
  background: var(--surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  text-decoration: none;
  color: var(--text-h);
  font-weight: 600;
  transition:
    transform var(--transition),
    box-shadow var(--transition),
    background var(--transition),
    color var(--transition);
}
.list-link-card:hover {
  transform:
    translateY(calc(-3px + var(--magnet-y, 0px)))
    translateX(calc(4px + var(--magnet-x, 0px)));
  box-shadow: var(--shadow-md);
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
}
.list-link-card:active {
  transform:
    translateY(calc(1px + var(--magnet-y, 0px)))
    translateX(calc(2px + var(--magnet-x, 0px)))
    scale(0.99, 0.97);
  box-shadow: var(--shadow-press);
}
.list-link-card__arrow {
  color: var(--color-primary);
  font-size: 18px;
  flex-shrink: 0;
}
/* Empty-inside state (section not yet populated with content) — card stays visible but
   unclickable, instead of leading to a dead end. */
.list-link-card--disabled {
  cursor: default;
  opacity: 0.55;
  box-shadow: none;
}
.list-link-card--disabled:hover,
.list-link-card--disabled:active {
  transform: none;
  background: var(--surface);
  color: var(--text-h);
  box-shadow: none;
}
.list-link-card__hint {
  font-size: 13px;
  font-weight: 400;
  color: var(--text-muted);
}
/* Forms */
.field {
  margin-bottom: 16px;
  text-align: left;
}
.field label {
  display: block;
  margin-bottom: 6px;
  font-weight: 600;
  color: var(--text-h);
  font-size: 14px;
}
.input {
  width: 100%;
  padding: 10px 14px;
  font-size: 15px;
  font-family: var(--sans);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-h);
  box-sizing: border-box;
  transition:
    border-color var(--transition),
    box-shadow var(--transition);
}
.input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-light);
}
.otp-input {
  font-size: 22px;
  text-align: center;
  letter-spacing: 8px;
  font-weight: 700;
}
.form-error {
  background: var(--color-danger-light);
  color: var(--color-danger);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  font-size: 14px;
  margin-bottom: 16px;
  font-weight: 500;
}
.form-success {
  background: var(--color-secondary-light);
  color: var(--color-secondary-dark);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  font-size: 14px;
  margin-bottom: 16px;
  font-weight: 500;
}
/* Badges */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: var(--radius-full);
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
}
.badge-success {
  background: var(--color-secondary-light);
  color: var(--color-secondary-dark);
}
.badge-warning {
  background: var(--color-accent-light);
  color: var(--color-accent-dark);
}
.badge-neutral {
  background: var(--border);
  color: var(--text-muted);
}
/* App chrome: header / sidebar / footer */
.app-shell {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: var(--bg);
}
/* Glassmorphism: translucent + blurred rather than opaque, so the themed gradient behind it
   (see .app-main below) shows through faintly. color-mix() keeps this theme-aware for free —
   each theme's own --surface still shows through its own tint instead of a hardcoded white. */
.app-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 28px;
  min-height: 68px;
  background: color-mix(in srgb, var(--surface) 75%, transparent);
  /* -webkit- fallback before the standard property, not after: Vite's prod CSS minifier
     (esbuild) treats same-value prefixed/unprefixed pairs as duplicate declarations and drops
     the earlier one — with the standard property first (as it was), prod builds shipped only
     -webkit-backdrop-filter, which Firefox (and anything else that doesn't honor the WebKit
     prefix) silently ignores, leaving the header fully see-through there. Verified on the live
     prod bundle. */
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  position: sticky;
  top: 0;
  z-index: 10;
}
.app-header__brand {
  font-weight: 800;
  font-size: 20px;
  color: var(--color-primary-dark);
  display: flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
}
.app-body {
  display: flex;
  flex: 1;
  align-items: stretch;
}
/* Wraps .app-main + .app-footer (see App.tsx) as a single .app-body flex child — keeps the
   footer's height inside .sidebar's sticky containing block (.app-body itself), see .sidebar's
   own comment above for why that matters. flex: 1 here (not on .app-main) is what makes
   .app-body stretch to the page's full content height same as before. */
.app-main-col {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}
.app-main {
  flex: 1;
  position: relative;
}
/* Default/boy/girl site-wide gradient backgrounds. All three read their colors straight off
   that theme's own -light tokens (variables.css) instead of hardcoding hex here, so the
   gradient always matches the theme's palette automatically. Default's is deliberately the
   most subtle (barely-there wash, kid-of-any-gender/age — this is the "no theme picked" theme
   the site). Boy/girl used to be per-page illustration art (url() webp, scoped to one page
   via :has(.page--*)) in host/trainings/learning's own components files — replaced 2026-10-20
   with this site-wide gradient; those old assets are left on disk unused, not deleted.
   Notebook/blackboard are unaffected and stay defined locally per app (see those files) since
   their art is a real raster asset with an app-relative url() path. */
:root[data-theme="default"] .app-main,
:root:not([data-theme]) .app-main {
  background-image:
    radial-gradient(at 14% 18%, var(--color-primary-light) 0%, transparent 55%),
    radial-gradient(at 86% 14%, var(--color-secondary-light) 0%, transparent 55%),
    radial-gradient(at 50% 100%, var(--color-accent-light) 0%, transparent 60%);
  background-color: var(--bg);
  background-attachment: local;
}
:root[data-theme="boy"] .app-main {
  background-image:
    radial-gradient(at 12% 15%, var(--color-primary-light) 0%, transparent 58%),
    radial-gradient(at 88% 20%, var(--color-secondary-light) 0%, transparent 55%),
    radial-gradient(at 50% 105%, var(--color-accent-light) 0%, transparent 62%);
  background-color: var(--bg);
  background-attachment: local;
}
:root[data-theme="girl"] .app-main {
  background-image:
    radial-gradient(at 12% 15%, var(--color-primary-light) 0%, transparent 58%),
    radial-gradient(at 88% 20%, var(--color-secondary-light) 0%, transparent 55%),
    radial-gradient(at 50% 105%, var(--color-accent-light) 0%, transparent 62%);
  background-color: var(--bg);
  background-attachment: local;
}
.app-footer {
  padding: 14px 28px;
  text-align: center;
  color: var(--text-muted);
  font-size: 13px;
  background: var(--surface);
  border-top: 1px solid var(--border);
}
/* Sticky to the viewport (not just the top of the page) so .sidebar__footer below — the FAQ/
   contact links — stays on screen without scrolling the whole page, same reasoning as
   .app-header's own sticky. top/height line up with .app-header's min-height (68px) so the
   sidebar starts right under the header instead of sticking under it. align-self: flex-start
   opts out of .app-body's align-items: stretch, which would otherwise force .sidebar to match
   .app-main's full (possibly page-length) height and defeat the sticky height cap. Mobile
   (<=768px) overrides position/top/height back to a fixed 100vh drawer — see components.scss.
   z-index: unlike position: relative, position: sticky always opens its own stacking context —
   without an explicit z-index here (auto), .app-main (later in DOM, same root stacking context)
   paints over it, which visually swallows anything spilling past .sidebar's right edge or top
   edge (e.g. .tooltip__bubble, z-index 50 — that z-index only wins contests *inside* .sidebar's
   own stacking context now, not against .app-main/.app-header). Must beat .app-header's z-index
   (10) too, not just .app-main's implicit 0 — a tooltip on one of the top nav items opens
   upward (.tooltip__bubble's bottom: calc(100% + 8px)) right into the header's rectangle.
   .sidebar and .app-header never otherwise visually overlap (header is 0–68px, sidebar's sticky
   top starts at 68px), so winning that contest doesn't cost anything elsewhere. */
.sidebar {
  width: 240px;
  flex-shrink: 0;
  align-self: flex-start;
  position: sticky;
  top: 68px;
  height: calc(100vh - 68px);
  z-index: 11;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 20px 12px;
  box-sizing: border-box;
  overflow: visible;
  transition: width var(--transition), padding var(--transition);
}
.sidebar__nav {
  flex: 1 1 auto;
}
/* Pinned to the bottom of the (now viewport-height) .sidebar via margin-top: auto — same nav
   item markup/classes as the main list above, just visually separated by a divider. */
.sidebar__footer {
  margin-top: auto;
  flex-shrink: 0;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
/* Collapsed sidebar on wide screens (toggle lives in front/host — see App.tsx Layout
   sidebarCollapsed). Shrinks to an icon-only strip (56px), not 0 — labels hide, icons stay
   centered, so the menu frees up space for the main content (e.g. the lesson-room drawing
   canvas) but stays clickable without expanding first. */
.sidebar--collapsed {
  width: 56px;
  padding: 20px 8px;
}
.sidebar--collapsed .sidebar__link {
  justify-content: center;
  padding: 6px 0;
}
.sidebar--collapsed .sidebar__link-label {
  display: none;
}
.sidebar__nav-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.sidebar__link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border-radius: var(--radius-md);
  color: var(--text);
  text-decoration: none;
  font-weight: 600;
  font-size: 15px;
  transition:
    background var(--transition),
    color var(--transition),
    transform var(--transition);
}
.sidebar__link-icon {
  font-size: 18px;
  line-height: 1;
  flex-shrink: 0;
}
.sidebar__link:hover {
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
}
.sidebar__link:active {
  transform: scale(0.97);
}
.sidebar__link--active {
  background: var(--color-primary);
  color: #fff;
  box-shadow: var(--shadow-sm);
}
.sidebar__link--active:hover {
  background: var(--color-primary-dark);
  color: #fff;
}
/* Tooltip — CSS-only hover/focus popup, no third-party library (none in the repo anyway).
   Shared verbatim between host (Sidebar hint, input-field labels) and learning. */
.tooltip {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: help;
}
.tooltip__icon {
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.tooltip__bubble {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  min-width: 200px;
  max-width: 260px;
  background: var(--surface);
  color: var(--text);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: 10px 12px;
  font-size: 13px;
  font-weight: 400;
  line-height: 1.4;
  text-align: left;
  z-index: 50;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--transition), transform var(--transition);
  pointer-events: none;
}
.tooltip:hover .tooltip__bubble,
.tooltip:focus .tooltip__bubble,
.tooltip:focus-within .tooltip__bubble {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
}
/* User switcher (child picker in header) — base trigger/dropdown only. Host adds its own
   extra rules locally (glow animation, theme sections, theme-option) since only host's
   UserSwitcher component renders that richer dropdown content. */
.user-switcher {
  position: relative;
}
.user-switcher__trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
  border: none;
  border-radius: var(--radius-full);
  font-weight: 700;
  font-size: 14px;
  font-family: var(--sans);
  cursor: pointer;
  transition: background var(--transition), color var(--transition), transform var(--transition);
}
.user-switcher__trigger:hover {
  background: var(--color-primary);
  color: #fff;
  transform: translateY(-1px);
}
.user-switcher__trigger:active {
  transform: translateY(0) scale(0.97);
}
/* Entrance pop: scale/opacity from the top-right corner (where the trigger sits), since this
   is a conditionally-mounted element (see UserSwitcher in App.tsx) — no exit animation without
   JS state, so this only covers open, not close. */
@keyframes dropdown-pop {
  from {
    opacity: 0;
    transform: scale(0.95) translateY(-6px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}
.user-switcher__dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  /* Prefix before standard — see .app-header's comment on this same pair for why the order
     matters to the prod minifier. */
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  list-style: none;
  margin: 0;
  padding: 6px;
  min-width: 200px;
  z-index: 20;
  transform-origin: top right;
  animation: dropdown-pop 0.15s ease-out;
}
.user-switcher__dropdown li {
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-weight: 500;
  transition: background var(--transition), color var(--transition);
}
.user-switcher__dropdown li:hover {
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
}
/* Modal */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(29, 43, 58, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 16px;
}
.modal {
  background: var(--surface);
  padding: 32px;
  border-radius: var(--radius-lg);
  max-width: 420px;
  width: 100%;
  box-shadow: var(--shadow-lg);
  text-align: left;
}
/* Misc */
.loading-state {
  text-align: center;
  margin-top: 40px;
  color: var(--text-muted);
  font-size: 16px;
}
.empty-state {
  color: var(--text-muted);
  font-size: 15px;
  padding: 20px 0;
}
.stack {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.gap-sm {
  gap: 8px;
}
.mt-sm {
  margin-top: 8px;
}
.mt-md {
  margin-top: 16px;
}
.mt-lg {
  margin-top: 24px;
}
/* Children management */
.child-card__name {
  font-weight: 700;
  font-size: 17px;
  color: var(--text-h);
}
.child-card__birthdate {
  font-weight: 400;
  font-size: 14px;
  color: var(--text-muted);
}
.stats-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}
.stats-table th {
  text-align: left;
  padding: 8px 10px;
  background: var(--bg);
  color: var(--text-muted);
  font-weight: 600;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.stats-table th:not(:first-child),
.stats-table td:not(:first-child) {
  text-align: center;
}
.stats-table td {
  padding: 8px 10px;
  border-top: 1px solid var(--border);
}
.stats-table__num {
  font-weight: 600;
  color: var(--text-h);
}
/* Training session */
.progress-bar {
  height: 8px;
  border-radius: var(--radius-full);
  background: var(--border);
  overflow: hidden;
  margin-top: 24px;
}
.progress-bar__fill {
  height: 100%;
  background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
  transition: width 0.3s ease;
}
.progress-label {
  margin-top: 8px;
  font-size: 13px;
  color: var(--text-muted);
  font-weight: 600;
}
.question-card {
  font-size: 24px;
  padding: 24px;
  background: var(--color-primary-light);
  border-radius: var(--radius-lg);
  text-align: center;
  font-weight: 700;
  color: var(--text-h);
}
.word-chip {
  padding: 8px 18px;
  border: none;
  border-radius: var(--radius-full);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  font-size: 15px;
  font-weight: 600;
  font-family: var(--sans);
  cursor: pointer;
  transition:
    transform var(--transition),
    box-shadow var(--transition),
    background var(--transition),
    opacity var(--transition);
}
.word-chip:hover:not(:disabled) {
  background: var(--color-primary-light);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
.word-chip:disabled {
  opacity: 0.5;
  cursor: default;
}
.word-chip--selected {
  background: var(--color-secondary-light);
  color: var(--color-secondary-dark);
}
.sentence-drop-zone {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  min-height: 48px;
  border: 2px dashed var(--border);
  padding: 12px;
  border-radius: var(--radius-md);
  background: var(--bg);
}
.feedback {
  font-size: 17px;
  font-weight: 700;
  padding: 14px 18px;
  border-radius: var(--radius-md);
  margin-top: 16px;
}
/* This is the single most frequent reward/correction moment in the whole app — it fires on
   every answer, in every exercise type, across trainings/learning/daily-activity. Correct
   gets a playful overshoot pop (the bounce sells "nailed it"); incorrect is deliberately
   calmer/flatter, no overshoot — a wrong answer shouldn't feel startling. Both rely on the
   div only existing in the DOM once feedback is set (conditional render in the parent), so
   the animation just plays on mount, same trick as .user-switcher__dropdown's dropdown-pop.
   2026-10-20. */
.feedback--correct {
  background: var(--color-secondary-light);
  color: var(--color-secondary-dark);
  animation: feedback-correct-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes feedback-correct-pop {
  0% {
    opacity: 0;
    transform: scale(0.8);
  }
  60% {
    opacity: 1;
    transform: scale(1.05);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}
.feedback--incorrect {
  background: var(--color-danger-light);
  color: var(--color-danger);
  animation: feedback-fade-in 0.3s ease-out;
}
@keyframes feedback-fade-in {
  from {
    opacity: 0;
    transform: translateY(2px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
/* Exercise answer buttons get the loudest tactile tier (vs. the calmer .btn/.sidebar__link) —
   this is the actual training interaction, so feedback here should feel the most game-like.
   Duolingo-style "3D button": a solid (non-blurred) box-shadow lip under the button instead of
   a border, so the press travel below can collapse the lip without any layout shift (a
   border-bottom of the same size would resize the box on press). Squash (scaleY<1, scaleX>1)
   on press sells the "pushed in" feel further. 2026-10-20. */
.answer-choice-btn {
  padding: 10px 22px;
  margin-right: 8px;
  font-size: 18px;
  font-weight: 700;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text-h);
  cursor: pointer;
  box-shadow: 0 4px 0 0 var(--border);
  transition:
    background var(--transition),
    border-color var(--transition),
    box-shadow var(--transition),
    transform var(--transition);
}
.answer-choice-btn:hover:not(:disabled) {
  border-color: var(--color-primary);
  background: var(--color-primary-light);
  box-shadow: 0 4px 0 0 var(--color-primary);
  transform: translateY(-2px);
}
.answer-choice-btn:active:not(:disabled) {
  box-shadow: 0 1px 0 0 var(--color-primary);
  transform: translateY(3px) scale(1.03, 0.93);
}
.answer-choice-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  box-shadow: none;
}
.answer-input {
  padding: 10px 14px;
  font-size: 18px;
  width: 100%;
  max-width: 220px;
  font-family: var(--sans);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  box-sizing: border-box;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.answer-input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-light);
}
.text-center {
  text-align: center;
}
.text-muted {
  color: var(--text-muted);
}
.row--center {
  justify-content: center;
}
.row--between {
  justify-content: space-between;
}
.instruction-text {
  font-size: 17px;
  color: var(--text);
}
.word-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.selected-word-chip {
  background: var(--color-secondary-light);
  color: var(--color-secondary-dark);
  padding: 6px 14px;
  border-radius: var(--radius-full);
  font-weight: 600;
}
/* ===================== Exercise rendering (trainings + learning) =====================
   Shared verbatim between front/trainings and front/learning (both render the same
   TrainingService.Example-driven exercise UI — see Lesson content = generated examples note),
   each file's comments already cross-referenced the other as "same trick/grid, duplicated for
   занятия" before this got centralized. Inert in host/adminPanel/city/daily-activity. */
/* Кучки предметов — сложение/вычитание до 10/20, сравнение, пропущенное число.
   --pile-cell — единственное место, которое задаёт физический размер ячейки: раскладка в
   itemVariants.ts (computePilePositions) выражена в "клетках" (calc(N * var(--pile-cell))),
   поэтому уменьшение переменной по media query пропорционально сжимает всю кучку —
   и картинки, и наложение рядов — без отдельного пересчёта в JS и без скролла. */
.item-piles {
  --pile-cell: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  flex-wrap: nowrap;
  max-width: 100%;
  overflow-x: auto;
  /* Резервируем высоту под самую высокую возможную кучку (5 рядов при MAX_VISUAL_COUNT=20,
     см. computePilePositions в itemVariants.ts), чтобы область с картинками всегда занимала
     одно и то же место на странице — всё, что ниже (поле ввода, кнопка "Проверить"), не
     должно сдвигаться от примера к примеру. */
  min-height: calc(3.4 * var(--pile-cell) + 16px);
}
.item-pile {
  position: relative;
  min-height: 40px;
  padding: 8px;
  border-radius: var(--radius-md);
  background: var(--color-primary-light);
}
.item-pile__icon {
  position: absolute;
  width: var(--pile-cell);
  height: var(--pile-cell);
  object-fit: contain;
}
.item-pile--hidden {
  display: flex;
  width: var(--pile-cell);
  height: 40px;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  font-weight: 700;
  color: var(--text-muted);
  background: var(--bg);
  border: 2px dashed var(--border);
}
.item-piles__op {
  font-size: 22px;
  font-weight: 700;
  color: var(--text-h);
}
/* Таблица умножения — a контейнеров по b предметов в каждом. */
.mult-visual {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: center;
  gap: 12px;
}
.mult-visual__container {
  position: relative;
  width: 72px;
}
.mult-visual__container-img {
  width: 100%;
  height: auto;
  display: block;
}
.mult-visual__items {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 2px;
  margin-top: -18px;
}
.mult-visual__item-img {
  width: 18px;
  height: 18px;
  object-fit: contain;
}
/* Сложение/вычитание столбиком — фиксированная разрядная сетка: каждая цифра
   (или пустая заполняющая ячейка для более коротких чисел) занимает одну и ту же
   ширину, поэтому единицы/десятки/сотни визуально выстраиваются друг под другом. */
.column-arithmetic {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-end;
}
.column-arithmetic__row {
  display: flex;
}
.column-arithmetic__operator {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  font-size: 26px;
  font-weight: 700;
  color: var(--text-h);
}
.column-arithmetic__cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 40px;
  font-size: 24px;
  font-weight: 700;
  color: var(--text-h);
  font-variant-numeric: tabular-nums;
}
.column-arithmetic__rule {
  align-self: stretch;
  border-top: 3px solid var(--text-h);
  margin: 4px 0;
}
.column-digit-input {
  width: 36px;
  height: 40px;
  padding: 0;
  text-align: center;
  font-size: 22px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  box-sizing: border-box;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.column-digit-input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-light);
}
/* Пустая рамка-заполнитель над разрядами, которых нет в самом ответе (см. answerStart в
   TrainingSessionPage) — только контур, без фокуса/курсора/фона инпута. */
.column-digit-input--placeholder {
  display: inline-flex;
  border-style: dashed;
  border-color: var(--border);
  background: transparent;
  pointer-events: none;
}
/* "Вставь пропущенные буквы" (missing_letters) — обратный отсчёт, затем буквы "растворяются"
   и заменяются полями ввода; .missing-letters__cell/__input одной фиксированной ширины —
   иначе слово меняло бы ширину в пикселях в момент замены буквы на инпут. */
.missing-letters__countdown {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 56px;
  height: 56px;
  padding: 0 8px;
  border-radius: 50%;
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
  font-size: 30px;
  font-weight: 800;
  margin-bottom: 12px;
  animation: missingLettersTick 0.4s ease;
}
/* Занимает тот же слот (min-width/height), что и тикающая цифра, — предложение под ним
   не сдвигается, когда отсчёт сменяется этой надписью. Шире текста — border-radius: 50%
   на невысоком широком блоке сам даёт форму таблетки со скруглёнными краями. */
.missing-letters__countdown--done {
  font-size: 18px;
}
@keyframes missingLettersTick {
  from { transform: scale(1.35); opacity: 0.3; }
  to { transform: scale(1); opacity: 1; }
}
.missing-letters__sentence {
  font-size: 22px;
  line-height: 2.4;
  color: var(--text-h);
}
.missing-letters__word {
  display: inline-flex;
  align-items: baseline;
  white-space: nowrap;
  /* Явный зазор между словами — раньше роль разделителя играл единственный &nbsp;, который
     визуально был почти не шире промежутков между буквенными ячейками (см. .missing-letters__cell),
     из-за чего пробелы между словами сливались с пробелами между буквами. */
  margin-right: 14px;
}
.missing-letters__cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Чуть уже буквы+инпута "в размер" (а не комфортный зазор под клик) — раньше более
     широкая ячейка визуально растягивала буквы внутри слова почти как межсловный пробел. */
  width: 17px;
  height: 32px;
}
.missing-letters__cell--dissolving {
  /* Длительность/задержка — инлайн-стилем из компонента (своя у каждой буквы, см.
     STAGGER_MS), поэтому тут только имя анимации и easing/fill-mode. */
  animation-name: missingLettersDissolve;
  animation-timing-function: ease-in;
  animation-fill-mode: forwards;
}
@keyframes missingLettersDissolve {
  to { opacity: 0; filter: blur(3px); transform: scale(0.7); }
}
.missing-letters__input {
  padding: 0;
  text-align: center;
  font-size: 18px;
  font-weight: 700;
  font-family: inherit;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  box-sizing: border-box;
  background: var(--surface);
  color: var(--text-h);
  /* "Проявление" — обратный эффект растворению (missingLettersDissolve), длительность тоже
     задаётся инлайн-стилем из компонента, чтобы совпадать с DISSOLVE_MS. */
  animation-name: missingLettersAppear;
  animation-timing-function: ease-out;
}
@keyframes missingLettersAppear {
  from { opacity: 0; filter: blur(3px); transform: scale(0.7); }
  to { opacity: 1; filter: blur(0); transform: scale(1); }
}
.missing-letters__input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-light);
}
/* Геометрия (периметр) — сюжетная иллюстрация из пула (GEOMETRY_VARIANTS) с двумя
   подписями сторон, позиционированными в % от картинки (см. sideALabel/sideBLabel). */
.geometry-illustration {
  position: relative;
  width: 100%;
  max-width: 360px;
  margin: 0 auto;
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.geometry-illustration__img {
  display: block;
  width: 100%;
  height: auto;
}
.geometry-illustration__label {
  position: absolute;
  transform: translate(-50%, -50%);
  background: var(--surface);
  color: var(--text-h);
  font-weight: 700;
  font-size: 14px;
  padding: 3px 10px;
  border-radius: var(--radius-full);
  box-shadow: var(--shadow-md);
  white-space: nowrap;
}
/* ===== Responsive (phone/tablet) ===== */
@media (max-width: 768px) {
  .page {
    padding: 20px 16px 40px;
  }
  .item-piles {
    --pile-cell: 48px;
  }
}
@media (max-width: 480px) {
  .card {
    padding: 16px 18px;
  }
  .item-piles {
    --pile-cell: 36px;
  }
}
.field label .tooltip {
  margin-left: 6px;
  vertical-align: middle;
}
/* Error banner pinned to the top of the page (e.g. "service unavailable") — kept local since
   adminPanel is the only other app that also renders one; not worth centralizing a 2-app dup. */
.service-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 10px 16px;
  background: var(--color-danger-light);
  color: var(--color-danger);
  font-size: 14px;
  font-weight: 500;
  text-align: center;
  position: sticky;
  top: 0;
  z-index: 20;
}
.service-banner__close {
  background: none;
  border: none;
  color: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
}
.app-header__actions {
  display: flex;
  align-items: center;
  gap: 12px;
  /* Единственный блок, поглощающий свободное место — иначе (например, через
     justify-content: space-between на .app-header) ширина этого блока меняется
     вместе с длиной имени ребёнка в UserSwitcher и сдвигает всё, что стоит перед
     ним, включая .app-header__brand ("kek.academy"). */
  margin-left: auto;
}
/* Скрыта на широких экранах — раскрывается только в мобильном breakpoint ниже. */
.app-header__burger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border: none;
  border-radius: var(--radius-md);
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
/* Обратная видимость относительно .app-header__burger выше: видна на широких экранах
   (сворачивает постоянно видимый сайдбар), скрыта в мобильном breakpoint ниже — там
   сайдбар и так drawer поверх контента, сворачивать нечего (см. Sidebar--collapsed). */
.app-header__sidebar-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border: none;
  border-radius: var(--radius-md);
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.app-header__sidebar-toggle:hover {
  background: var(--color-primary);
  color: #fff;
}
/* Затемнение позади выезжающего меню на мобильных — само меню и подложка
   показываются только в мобильном breakpoint ниже (см. .sidebar/.sidebar--open). */
.sidebar__backdrop {
  display: none;
}
.logout-button {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  background: var(--color-danger-light);
  color: var(--color-danger);
  border: none;
  border-radius: var(--radius-full);
  font-weight: 700;
  font-size: 14px;
  font-family: var(--sans);
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}
.logout-button:hover {
  background: var(--color-danger);
  color: #fff;
}
.theme-loading-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  z-index: 5;
}
.theme-loading-overlay__spinner {
  width: 36px;
  height: 36px;
  border: 4px solid var(--border);
  border-top-color: var(--color-primary);
  border-radius: var(--radius-full);
  animation: theme-loading-spin 0.7s linear infinite;
}
@keyframes theme-loading-spin {
  to {
    transform: rotate(360deg);
  }
}
/* Доп. правило для .sidebar--collapsed (база — в shared-styles/components.css): прячет
   иконку-подсказку рядом с пунктом меню, т.к. .sidebar__item/.sidebar__item-hint — тоже
   host-только классы (см. ниже). */
.sidebar--collapsed .sidebar__item-hint {
  display: none;
}
.sidebar__item {
  display: flex;
  align-items: center;
  gap: 4px;
}
.sidebar__item .sidebar__link {
  flex: 1;
}
.sidebar__item-hint {
  flex-shrink: 0;
}
/* User switcher: richer dropdown content (themes list) — only host's UserSwitcher renders
   this, base trigger/dropdown/li is in shared-styles/components.css. */
/* Подсказка "нажми сюда" на странице-заглушке /trainings, пока ребёнок не выбран
   (см. UserSwitcher.shouldGlow в App.tsx) — пульсирующее кольцо вокруг кнопки. */
.user-switcher__trigger--glow {
  animation: user-switcher-glow 1.6s ease-in-out infinite;
}
@keyframes user-switcher-glow {
  0%, 100% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 55%, transparent);
  }
  50% {
    box-shadow: 0 0 0 8px color-mix(in srgb, var(--color-primary) 0%, transparent);
  }
}
.user-switcher__dropdown ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.user-switcher__section + .user-switcher__section {
  margin-top: 6px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.user-switcher__section-label {
  padding: 4px 10px 2px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
.user-switcher__themes {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.theme-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: background var(--transition), color var(--transition), transform var(--transition);
}
.theme-option:hover {
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
  transform: scale(1.02);
}
.theme-option:active {
  transform: scale(0.98);
}
.theme-option--active {
  background: var(--color-primary);
  color: #fff;
  font-weight: 700;
}
.theme-option--active::after {
  content: "✓";
}
/* ===== Character companion + tic-tac-toe ===== */
@keyframes characterSlideIn {
  from {
    transform: translateX(-70px);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}
@keyframes characterSlideOut {
  from {
    transform: translateX(0);
    opacity: 1;
  }
  to {
    transform: translateX(-70px);
    opacity: 0;
  }
}
@keyframes characterBounce {
  0%, 100% {
    transform: translateY(0) rotate(-2deg);
  }
  50% {
    transform: translateY(-10px) rotate(2deg);
  }
}
@keyframes bubbleFadeIn {
  from {
    transform: translateY(6px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}
.character-companion {
  position: fixed;
  left: 216px;
  bottom: 24px;
  z-index: 900;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  animation: characterSlideIn 0.5s ease-out;
}
.character-companion--leaving {
  animation: characterSlideOut 0.4s ease-in forwards;
}
.character-companion__close {
  position: absolute;
  top: -10px;
  right: -10px;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  line-height: 1;
  font-size: 14px;
}
.character-companion__sprite {
  width: 120px;
  height: auto;
  filter: drop-shadow(var(--shadow-md));
  animation: characterBounce 2.4s ease-in-out infinite;
}
.character-companion__play {
  padding: 6px 16px;
  font-size: 14px;
}
.speech-bubble {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: 8px 14px;
  max-width: 200px;
  text-align: center;
  font-size: 14px;
  animation: bubbleFadeIn 0.3s ease-out;
}
/* Сова Профессор Ух — тот же визуальный каркас, что и .character-companion, но справа
   (не .left), чтобы не перекрывать Трансформера/Эльзу, если оба триггера совпадут. */
.owl-companion {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 900;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  animation: characterSlideIn 0.5s ease-out;
}
.owl-companion--leaving {
  animation: characterSlideOut 0.4s ease-in forwards;
}
.owl-companion__close {
  position: absolute;
  top: -10px;
  right: -10px;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  line-height: 1;
  font-size: 14px;
}
.owl-companion__sprite {
  width: 120px;
  height: auto;
  filter: drop-shadow(var(--shadow-md));
  animation: characterBounce 2.4s ease-in-out infinite;
}
.tic-tac-toe-board {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin: 16px 0;
}
.tic-tac-toe-cell {
  appearance: none;
  -webkit-appearance: none;
  aspect-ratio: 1;
  font-size: 32px;
  font-weight: 800;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--color-primary-light);
  color: var(--text-h);
  cursor: pointer;
}
.tic-tac-toe-cell:disabled {
  cursor: default;
  opacity: 1;
}
.tic-tac-toe-cell--x {
  color: var(--color-primary-dark);
}
.tic-tac-toe-cell--o {
  color: var(--color-accent-dark);
}
.tic-tac-toe-result {
  text-align: center;
  font-weight: 600;
}
@media (max-width: 768px) {
  .character-companion {
    left: 12px;
    bottom: 16px;
  }
  .character-companion__sprite {
    width: 160px;
  }
  .owl-companion {
    right: 12px;
    bottom: 16px;
  }
  .owl-companion__sprite {
    width: 160px;
  }
  .speech-bubble {
    max-width: 190px;
    font-size: 14px;
  }
}
.ml-sm {
  margin-left: 8px;
}
/* ===== Responsive (phone/tablet) ===== */
@media (max-width: 768px) {
  .app-header {
    padding: 12px 16px;
    flex-wrap: wrap;
    row-gap: 8px;
  }
  .app-header__brand {
    font-size: 18px;
  }
  .app-header__actions {
    gap: 8px;
  }
  .app-header__burger {
    display: inline-flex;
  }
  /* Сайдбар уходит из потока (position: fixed) и выезжает как drawer поверх контента —
     .app-main занимает всю ширину сам по себе, без flex-direction: column. */
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    height: 100vh;
    width: 240px;
    max-width: 80vw;
    transform: translateX(-100%);
    transition: transform var(--transition, 0.25s ease);
    z-index: 20;
    overflow-y: auto;
    box-shadow: var(--shadow-md, 0 4px 16px rgba(0, 0, 0, 0.2));
  }
  .sidebar--open {
    transform: translateX(0);
  }
  .sidebar__backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    z-index: 19;
  }
  /* На узких экранах сайдбар — drawer поверх контента (position: fixed выше), а не flex-
     элемент .app-body — сворачивать тумблером нечего, он и так не занимает места, пока не
     открыт бургером. */
  .app-header__sidebar-toggle {
    display: none;
  }
}
@media (max-width: 480px) {
  .app-header__brand {
    font-size: 16px;
  }
  .user-switcher__trigger,
  .logout-button {
    padding: 7px 12px;
    font-size: 13px;
  }
  .user-switcher__dropdown {
    min-width: 0;
    max-width: calc(100vw - 32px);
  }
  .modal {
    padding: 20px;
  }
  .question-card {
    font-size: 19px;
    padding: 18px;
  }
  .stats-table {
    font-size: 12px;
  }
  .stats-table th,
  .stats-table td {
    padding: 6px 6px;
  }
}
/* ===================== Site themes: backgrounds =====================
   Token colors live in variables.scss; this is the imagery layer, applied to .app-main
   (the content area) rather than .app-shell: the header/sidebar/footer sit on top with an
   opaque --surface background anyway, so theming .app-shell just wasted most of the image
   behind that chrome. background-size: 100% 100% stretches the art to exactly fill .app-main,
   so it's always fully visible (no cropping) at any screen size. Notebook/blackboard are
   site-wide but kept local per app (not centralized): the url() asset paths below are
   relative to each app's own src/assets/themes/ folder, which only host/learning/trainings
   have. Kept in sync with learning/components.css and trainings/components.css.
   Default/boy/girl's site-wide gradient backgrounds moved to shared-styles/components.css
   (2026-10-20) — they're pure CSS (no raster asset), so centralizing has no per-app asset-path
   problem. The old per-page boy/girl illustrations (boy-family.webp etc.) are left on disk
   unused for now, not deleted. */
/* Notebook: checkered grid + red margin line + ink blot, pure CSS/SVG (no raster asset) */
:root[data-theme=notebook] .app-main {
  background-color: #f6efdf;
  background-image: linear-gradient(to right, rgba(174, 191, 214, 0.55) 1px, transparent 1px), linear-gradient(to bottom, rgba(174, 191, 214, 0.55) 1px, transparent 1px), linear-gradient(to right, transparent calc(9% - 1px), rgba(178, 69, 63, 0.55) calc(9% - 1px), rgba(178, 69, 63, 0.55) 9%, transparent 9%);
  background-size: 22px 22px, 22px 22px, 100% 100%;
  background-repeat: repeat, repeat, no-repeat;
  background-attachment: local, local, local;
  position: relative;
}
:root[data-theme=notebook] .app-main::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cfilter id='b' x='-20%25' y='-20%25' width='140%25' height='140%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' seed='7' result='n'/%3E%3CfeDisplacementMap in='SourceGraphic' in2='n' scale='6'/%3E%3C/filter%3E%3Cg filter='url(%23b)' fill='%232f4374' fill-opacity='0.12'%3E%3Ccircle cx='93' cy='90' r='7'/%3E%3Ccircle cx='97.5' cy='94' r='2.2'/%3E%3Ccircle cx='89' cy='95' r='1.4'/%3E%3C/g%3E%3Cg stroke='%23b2453f' stroke-opacity='0.55' stroke-width='0.6' fill='none' stroke-linecap='round'%3E%3Cpath d='M3.2 21 Q4.5 19.5 6 21 Q7.5 22.5 4.8 23.3'/%3E%3Cpath d='M2.8 43 L4.6 45.5 L7.6 41'/%3E%3C/g%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: 100% 100%;
  z-index: 0;
}
/* Blackboard: real generated art, site-wide (one narrower crop for small screens).
   Faded with a dark scrim (matching --bg) so it reads as a backdrop, not full-strength art. */
:root[data-theme=blackboard] .app-main {
  background-color: #1b1b19;
  background-image: linear-gradient(rgba(27, 27, 25, 0.28), rgba(27, 27, 25, 0.28)), url("/assets/board-BiIdtUAD.webp");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  background-attachment: local;
}
@media (max-width: 700px) {
  :root[data-theme=blackboard] .app-main {
    background-image: linear-gradient(rgba(27, 27, 25, 0.28), rgba(27, 27, 25, 0.28)), url("/assets/board-narrow-BD28tmGe.webp");
  }
}
/* Boy/girl used to have per-page illustration art here (boy-family.webp etc., scoped via
   :has(.page--family)) — replaced 2026-10-20 by a site-wide CSS gradient, see
   shared-styles/components.css. Assets left on disk unused, not deleted. */
/* Sidebar icons: Leonardo-generated art instead of emoji, re-skinned per theme (same subject
   silhouette, different art treatment/palette per theme — see assets/icons/<name>/) and rotated
   monthly within a theme (pose variants per theme). The --<name>-icon-<theme> vars are set once
   per mount in Sidebar.tsx (not per theme switch, see ICON_SETS there) — these rules just pick
   the right var off the existing :root[data-theme="…"] cascade, same mechanism as .app-main's
   theme backgrounds. book = "Занятия", family = "Моя семья", dartboard = "Тренировки",
   balloon = "Доп. материалы", house = "Город". "Админ-панель" intentionally still emoji (⚙️) —
   not redone yet. */
.sidebar__link-icon--book,
.sidebar__link-icon--family,
.sidebar__link-icon--dartboard,
.sidebar__link-icon--balloon,
.sidebar__link-icon--house {
  display: inline-block;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  width: 38px;
  height: 38px;
}
.sidebar__link-icon--book {
  background-image: var(--book-icon-default);
}
.sidebar__link-icon--family {
  background-image: var(--family-icon-default);
}
.sidebar__link-icon--dartboard {
  background-image: var(--dartboard-icon-default);
}
.sidebar__link-icon--balloon {
  background-image: var(--balloon-icon-default);
}
.sidebar__link-icon--house {
  background-image: var(--house-icon-default);
}
:root[data-theme=notebook] .sidebar__link-icon--book {
  background-image: var(--book-icon-notebook);
}
:root[data-theme=notebook] .sidebar__link-icon--family {
  background-image: var(--family-icon-notebook);
}
:root[data-theme=notebook] .sidebar__link-icon--dartboard {
  background-image: var(--dartboard-icon-notebook);
}
:root[data-theme=notebook] .sidebar__link-icon--balloon {
  background-image: var(--balloon-icon-notebook);
}
:root[data-theme=notebook] .sidebar__link-icon--house {
  background-image: var(--house-icon-notebook);
}
:root[data-theme=blackboard] .sidebar__link-icon--book {
  background-image: var(--book-icon-blackboard);
}
:root[data-theme=blackboard] .sidebar__link-icon--family {
  background-image: var(--family-icon-blackboard);
}
:root[data-theme=blackboard] .sidebar__link-icon--dartboard {
  background-image: var(--dartboard-icon-blackboard);
}
:root[data-theme=blackboard] .sidebar__link-icon--balloon {
  background-image: var(--balloon-icon-blackboard);
}
:root[data-theme=blackboard] .sidebar__link-icon--house {
  background-image: var(--house-icon-blackboard);
}
:root[data-theme=boy] .sidebar__link-icon--book {
  background-image: var(--book-icon-boy);
}
:root[data-theme=boy] .sidebar__link-icon--family {
  background-image: var(--family-icon-boy);
}
:root[data-theme=boy] .sidebar__link-icon--dartboard {
  background-image: var(--dartboard-icon-boy);
}
:root[data-theme=boy] .sidebar__link-icon--balloon {
  background-image: var(--balloon-icon-boy);
}
:root[data-theme=boy] .sidebar__link-icon--house {
  background-image: var(--house-icon-boy);
}
:root[data-theme=girl] .sidebar__link-icon--book {
  background-image: var(--book-icon-girl);
}
:root[data-theme=girl] .sidebar__link-icon--family {
  background-image: var(--family-icon-girl);
}
:root[data-theme=girl] .sidebar__link-icon--dartboard {
  background-image: var(--dartboard-icon-girl);
}
:root[data-theme=girl] .sidebar__link-icon--balloon {
  background-image: var(--balloon-icon-girl);
}
:root[data-theme=girl] .sidebar__link-icon--house {
  background-image: var(--house-icon-girl);
}
* {
  box-sizing: border-box;
}
body {
  margin: 0;
  font: 16px/1.5 var(--sans);
  letter-spacing: 0.1px;
  color-scheme: light dark;
  color: var(--text);
  background: var(--bg);
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
#root {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}
h1,
h2,
h3 {
  font-family: var(--sans);
  font-weight: 700;
  color: var(--text-h);
  margin: 0 0 12px;
}
h1 {
  font-size: 32px;
  letter-spacing: -0.4px;
}
h2 {
  font-size: 24px;
  letter-spacing: -0.24px;
}
h3 {
  font-size: 18px;
}
p {
  margin: 0 0 8px;
}
a {
  color: var(--color-primary);
}
button {
  font-family: var(--sans);
}
code {
  font-family: ui-monospace, Consolas, monospace;
  font-size: 14px;
  padding: 3px 6px;
  border-radius: 4px;
  background: var(--border);
  color: var(--text-h);
}