/* ==========================================================================
   Герой ребёнка — «Сказочный альбом» design system
   Tokens → reset → base type → components → motion → mode overrides.
   Every colour and size comes from a token; new hex in components = bug.
   ========================================================================== */

/* ------------------------------- 1. tokens ------------------------------- */

:root {
  --paper: #FFF6E9;
  --paper-deep: #FFEBD2;
  --surface: #FFFFFF;
  --ink: #2B2118;
  --ink-muted: #6B5B4D;
  --ink-faint: #7D6C5E;
  --line: #E8D9C3;
  /* ALBUM1: карандашный пунктир мокапа альбома (design/screen-album.html,
     бриф §5) — им нарисованы силуэты неоткрытых наклеек. Значение зафиксировано
     владельцем вместе с мокапом, поэтому это токен, а не новый цвет в коде. */
  --line-deep: #C9B59A;
  --scrim: rgba(43, 33, 24, .55);

  --amber: #FFC93C;  --amber-h: #FFD566;  --amber-p: #F0B31F;  --amber-d: #FFE7A8;  --amber-t: #FFF0CC;
  --coral: #FF6B4A;  --coral-h: #FF8468;  --coral-p: #E8532F;  --coral-d: #FFD1C6;  --coral-t: #FFE6DF;
  --green: #2E7D5B;  --green-h: #3A9570;  --green-p: #256A4C;  --green-d: #BFD9CD;  --green-t: #DFF0E8;
  --sky:    #4FA3D1; --sky-h:   #6BB6DE;  --sky-p:   #3B8BB8;  --sky-d:   #C9E4F2;  --sky-t:   #E2F1F9;
  --berry:  #C33C6E; --berry-h: #D4537F;  --berry-p: #A62F5C;  --berry-d: #EFBCCF;  --berry-t: #F7E3EC;

  --on-accent: var(--ink);           /* ink on amber/coral/sky; white on green/berry */
  --success: var(--green);
  --warning: var(--amber);
  --danger: var(--coral);
  --ring: #1D6FA3;

  --r-sm: 12px; --r-md: 20px; --r-lg: 28px; --r-xl: 36px; --r-pill: 999px;

  --sh-1: 0 1px 2px rgba(43,33,24,.08), 0 2px 8px rgba(43,33,24,.06);
  --sh-2: 0 2px 4px rgba(43,33,24,.08), 0 8px 20px rgba(43,33,24,.10);
  --sh-3: 0 4px 8px rgba(43,33,24,.10), 0 16px 36px rgba(43,33,24,.14);
  --sh-sticker: 0 2px 0 rgba(43,33,24,.06), 0 10px 22px rgba(43,33,24,.12);
  /* D5: тень поднятого листа — вверх, а не вниз: лист приходит снизу. */
  --sh-sheet: 0 -10px 40px rgba(43,33,24,.15);

  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px;
  --page-pad: var(--s4);
  --card-gap: var(--s3);

  /* --fs-small: «мелкая кнопка/подпись» между body и label. Токен существовал
   * в девяти правилах комнаты, но не был определён НИГДЕ — var() отваливался
   * в inherit, и «мелкие» подписи и кнопки ехали родительским кеглем (кнопка
   * «Включить» в редакторе при kid-режиме раздувалась до 22px и вылезала из
   * карточки на 320: гейт лестницы кадров поймал ВИДИМЫЙ вылет). */
  --fs-display: 28px; --fs-title: 20px; --fs-body: 16px; --fs-small: 14px; --fs-label: 13px; --fs-caption: 12px;

  --z-card: 10; --z-header: 100; --z-scrim: 800; --z-modal: 900;
  --z-toast: 950; --z-levelup: 1000; --z-confetti: 1100;

  /* Живой герой (R3). Плащ перекрывается стадией на корне SVG. */
  --cape: var(--coral);
  --hero-size: 108px;

  --dur-fast: 120ms; --dur-base: 200ms; --dur-slow: 320ms; --dur-pop: 420ms;
  --ease-out: cubic-bezier(.22, 1, .36, 1);
  --ease-spring: cubic-bezier(.34, 1.56, .64, 1);
  --ease-in: cubic-bezier(.4, 0, 1, 1);

  --font-display: 'Baloo 2', 'Trebuchet MS', sans-serif;
  --font-body: 'Nunito', 'Trebuchet MS', sans-serif;
}

[data-mode="kid"] {
  --fs-display: 40px; --fs-title: 28px; --fs-body: 22px; --fs-small: 20px; --fs-label: 18px; --fs-caption: 16px;
  --r-md: 26px; --r-lg: 34px; --r-xl: 44px;
  --page-pad: var(--s5);
  --card-gap: var(--s4);
  --sh-sticker: 0 3px 0 rgba(43,33,24,.07), 0 14px 28px rgba(43,33,24,.14);
}

/* -------------------------------- 2. reset ------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  min-height: 100dvh;
  background:
    radial-gradient(120% 60% at 12% 0%, var(--paper-deep) 0%, transparent 60%),
    radial-gradient(100% 50% at 100% 0%, var(--amber-t) 0%, transparent 55%),
    var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 0;
  -webkit-font-smoothing: antialiased;
  touch-action: manipulation;
}

h1, h2, h3, p, ul, ol, fieldset { margin: 0; padding: 0; }
ul, ol { list-style: none; }
fieldset { border: 0; min-width: 0; }
button { font: inherit; color: inherit; cursor: pointer; border: 0; background: none; }
input, select { font: inherit; color: inherit; }
img { max-width: 100%; display: block; }
a { color: inherit; }

:focus-visible { outline: 3px solid var(--ring); outline-offset: 3px; border-radius: var(--r-sm); }

.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* ------------------------------ 3. typography ---------------------------- */

h1, h2, h3, .display { font-family: var(--font-display); font-weight: 700; text-wrap: balance; }

.masthead { text-align: center; padding: var(--s6) var(--s4) var(--s5); }
.masthead__mark { font-size: calc(var(--fs-display) * 1.8); line-height: 1; }
.masthead__title { font-size: var(--fs-display); line-height: 1.2; margin-top: var(--s2); }
.masthead__sub { color: var(--ink-muted); font-size: var(--fs-body); margin-top: var(--s1); }

.footnote {
  color: var(--ink-muted);
  font-size: var(--fs-caption);
  text-align: center;
  padding: var(--s5) var(--s4) var(--s7);
}
.link { color: var(--ink); text-decoration-thickness: 2px; text-underline-offset: 2px; }

/* -------------------------------- 4. layout ------------------------------ */

.shell {
  max-width: 640px;
  margin: 0 auto;
  padding: 0 var(--page-pad) var(--s8);
}
.shell--narrow { max-width: 480px; }
.shell--kid { max-width: 560px; padding-bottom: var(--s7); }

.topbar {
  position: sticky; top: 0; z-index: var(--z-header);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  padding: var(--s3) var(--page-pad);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(6px);
  border-bottom: 2px solid var(--line);
}
.topbar__brand { min-width: 0; }
.topbar__actions { display: flex; align-items: center; gap: var(--s2); }

/* ------------------------------- 4b. brand ------------------------------- */

/* Знак бренда. Цвета частей — не селекторами: внутрь дерева <use> селекторы
 * снаружи не проходят, а переменные сквозь него наследуются. Размер в em, так
 * что один компонент растёт вместе со строкой вокруг себя. */
.kitmark {
  --kit-coat: var(--amber);
  --kit-muzzle: var(--paper);
  --kit-feature: var(--ink);
  width: 1.35em; height: 1.35em; display: inline-block;
  vertical-align: -.3em; flex: none;
}

.brand {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
  color: var(--ink);
}

/* Слоган под словесным знаком. Кириллица, поэтому Nunito, а не Baloo 2 —
 * дисплейная гарнитура кириллицы не имеет. */
.masthead__tagline {
  display: block; margin-top: var(--s1);
  font-family: var(--font-body); font-weight: 800;
  font-size: var(--fs-body); letter-spacing: .01em; color: var(--ink-muted);
}
.topbar__role {
  font-size: var(--fs-label); font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--ink-muted);
}

.section { margin-top: var(--s5); }
.section__title {
  font-size: var(--fs-title);
  display: flex; align-items: center; gap: var(--s2);
  margin-bottom: var(--s3);
}
.count {
  font-family: var(--font-body); font-size: var(--fs-caption); font-weight: 800;
  background: var(--amber); color: var(--ink);
  border-radius: var(--r-pill); padding: 2px 10px;
}

/* -------------------------------- 5. cards ------------------------------- */

.card {
  background: var(--surface);
  border: 4px solid var(--surface);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-sticker);
  padding: var(--s5);
  margin-bottom: var(--s4);
}
.card--intro { padding: var(--s4) var(--s5); }

.steps { display: grid; gap: var(--s3); }
.steps li { display: flex; align-items: center; gap: var(--s3); }
.steps__n {
  flex: 0 0 auto;
  width: 32px; height: 32px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--amber); color: var(--ink);
  font-family: var(--font-display); font-weight: 800;
}
.divider {
  height: 2px; background: var(--line); border-radius: var(--r-pill);
  /* В узкой оболочке паддинг 16px — отрицательный отступ 24px вылезал за край
   * и давал горизонтальную прокрутку. max() не даёт отступу стать шире
   * паддинга оболочки. */
  margin: var(--s5) max(calc(var(--s5) * -1), calc(var(--page-pad) * -1));
}

/* -------------------------------- 6. forms ------------------------------- */

.form { display: grid; gap: var(--s4); }
.form__title { font-size: var(--fs-title); }
.form__note { color: var(--ink-muted); font-size: var(--fs-caption); margin-top: calc(var(--s2) * -1); }
.form__row { display: flex; gap: var(--s2); }
.form__row--task .input--grow { flex: 1 1 auto; }
.form__row--3 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }
.form__row--3 .input:first-child { grid-column: 1 / -1; }
.form__row--2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }

.field { display: grid; gap: var(--s2); }
.field__label {
  font-size: var(--fs-label); font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-muted);
}
.field__hint { font-size: var(--fs-caption); color: var(--ink-muted); }

.input {
  width: 100%; min-height: 48px;
  padding: var(--s3) var(--s4);
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.input:focus { outline: none; border-color: var(--ink); box-shadow: var(--sh-1); }
.input--pin { letter-spacing: .4em; text-align: center; font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title); }
.input--emoji { width: 64px; flex: 0 0 auto; text-align: center; font-size: var(--fs-title); }
select.input { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--ink) 50%), linear-gradient(135deg, var(--ink) 50%, transparent 50%); background-position: calc(100% - 20px) 50%, calc(100% - 14px) 50%; background-size: 6px 6px; background-repeat: no-repeat; padding-right: var(--s6); }

/* ------------------------------- 7. buttons ------------------------------ */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 52px; min-width: 44px;
  padding: var(--s3) var(--s5);
  border-radius: var(--r-pill);
  font-family: var(--font-display); font-weight: 700; font-size: var(--fs-body);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(1px) scale(.98); }
.btn--primary { background: var(--amber); color: var(--on-accent); box-shadow: var(--sh-2); }
.btn--primary:hover { background: var(--amber-h); box-shadow: var(--sh-3); }
.btn--primary:active { background: var(--amber-p); }
.btn--quiet { background: var(--surface); color: var(--ink); box-shadow: inset 0 0 0 2px var(--ink); }
.btn--quiet:hover { box-shadow: inset 0 0 0 2px var(--ink), var(--sh-1); }
.btn--ghost { background: transparent; box-shadow: inset 0 0 0 2px var(--line); color: var(--ink-muted); }
.btn--ghost:hover { color: var(--ink); box-shadow: inset 0 0 0 2px var(--ink-muted); }
.btn--danger { background: var(--coral-d); color: var(--ink); }
.btn--danger:hover { background: var(--coral-t); }
.btn--sm { min-height: 44px; padding: var(--s2) var(--s4); font-size: var(--fs-caption); }
.btn--lg { min-height: 64px; }
.btn--xl { min-height: 56px; width: 100%; font-size: var(--fs-title); }
.btn--kid {
  min-height: 72px; width: 100%;
  background: var(--coral); color: var(--on-accent);
  box-shadow: var(--sh-sticker); font-size: var(--fs-body);
}
.btn--kid:hover { background: var(--coral-h); }
.btn--kid:active { background: var(--coral-p); }

/* ---------------------------- 8. pickers (setup) ------------------------- */

.pick-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(52px, 1fr)); gap: var(--s2); }
.pick { position: relative; display: block; cursor: pointer; }
.pick input { position: absolute; opacity: 0; inset: 0; }
.pick__face {
  display: grid; place-items: center;
  min-height: 52px; border-radius: var(--r-md);
  background: var(--paper); box-shadow: inset 0 0 0 2px var(--line);
  font-size: var(--fs-title);
  transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out);
}
.pick__face--green { background: var(--green); } .pick__face--coral { background: var(--coral); }
.pick__face--sky { background: var(--sky); }       .pick__face--berry { background: var(--berry); }
.pick__face--sun { background: var(--amber); }
.pick input:checked + .pick__face {
  box-shadow: inset 0 0 0 3px var(--ink), var(--sh-2);
  transform: rotate(-2deg) scale(1.04);
}
.pick input:focus-visible + .pick__face { outline: 3px solid var(--ring); outline-offset: 3px; }

/* Вид героя (R6): в плитке сам герой и подпись — родитель выбирает глазами,
 * а не по памяти. Плитка шире аватарной: герой + слово «Дракончик» в строку.
 * 104px давало ряд 3+1 на телефоне; 120px держит два ряда по две. */
.pick-grid--species { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--s3); }
.pick--species .pick__face {
  min-height: 52px; padding: var(--s2) var(--s3);
  grid-template-rows: auto auto; gap: 2px;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-body); color: var(--ink);
}
.pick--species .pick__face .hero { --hero-size: 56px; }
.pick--species .pick__name { font-size: var(--fs-caption); }

/* R9: герой в панели наряда — родитель видит результат сохранения сразу, без
 * похода на детский экран. Размер между плиткой вида и героем на детском
 * экране: достаточно, чтобы разглядеть очки и шарфик, недостаточно, чтобы
 * панель распухла. */
.wearpreview {
  display: grid; place-items: center;
  padding: var(--s4); margin-bottom: var(--s4);
  border-radius: var(--r-md); background: var(--paper);
}
.wearpreview .hero { --hero-size: 108px; }

/* --------------------------------- 9. flash ------------------------------ */

.flash {
  padding: var(--s3) var(--s4);
  border-radius: var(--r-md);
  background: var(--coral-t);
  box-shadow: inset 0 0 0 2px var(--coral-d);
  color: var(--ink);
  margin-bottom: var(--s4);
}

/* ------------------------------- 10. topbar ------------------------------ */

.panel { background: var(--surface); border-radius: var(--r-lg); box-shadow: var(--sh-1); overflow: hidden; }
.panel__summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s4); min-height: 52px;
  font-family: var(--font-display); font-weight: 700; color: var(--ink-muted);
}
.panel__summary::-webkit-details-marker { display: none; }
.panel__summary:hover { color: var(--ink); }
.panel__plus {
  display: grid; place-items: center; width: 32px; height: 32px;
  border-radius: var(--r-pill); background: var(--amber); color: var(--ink);
}
.panel[open] .panel__summary { border-bottom: 2px solid var(--line); }
.panel form { padding: var(--s4); }
.panel .form { gap: var(--s3); }
/* TP1: список полон — панель остаётся, формы в ней нет. Тихая бумага вместо
 * жёлтого «плюса»: предложение добавить нечего, а объяснение — есть. */

.tmpl-row { display: flex; gap: var(--s2); overflow-x: auto; padding-bottom: var(--s2); scrollbar-width: none; }
.tmpl-row::-webkit-scrollbar { display: none; }
.tmpl {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: 44px; padding: var(--s2) var(--s3);
  background: var(--paper); border-radius: var(--r-pill);
  box-shadow: inset 0 0 0 2px var(--line);
  font-size: var(--fs-caption); font-weight: 700;
}
.tmpl:hover { box-shadow: inset 0 0 0 2px var(--ink-muted); }
.tmpl__emoji { font-size: var(--fs-body); }

/* ------------------------------- 11. pending ----------------------------- */

.pendinglist { display: grid; gap: var(--s3); }
.pending {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3);
  background: var(--surface);
  border: 4px solid var(--surface); border-radius: var(--r-lg);
  box-shadow: var(--sh-sticker);
  padding: var(--s3);
  animation: sticker-in var(--dur-slow) var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 45ms);
}
.pending__emoji {
  display: grid; place-items: center;
  width: 48px; height: 48px; border-radius: var(--r-md);
  background: var(--amber-t); font-size: var(--fs-title);
}
.pending__body { flex: 1 1 140px; display: grid; gap: 2px; }
.pending__title { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-body); }
.pending__meta { font-size: var(--fs-caption); color: var(--ink-muted); }
/* Кнопки — своей строкой на всю ширину: на 390px рядом с именем они не
 * помещаются, а перенос «как получится» каждый раз складывает их по-новому.
 * Обе растягиваются поровну: палец попадает, не целясь. */
.pending__actions { flex: 1 0 100%; display: flex; gap: var(--s2); }
.pending__actions form { flex: 1 1 0; display: flex; }
.pending__actions .btn { width: 100%; }
.chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 10px 2px 4px; border-radius: var(--r-pill);
  font-weight: 700; color: var(--ink);
}
.chip__emoji { font-size: var(--fs-body); }
.chip--green { background: var(--green-t); } .chip--coral { background: var(--coral-t); }
.chip--sky { background: var(--sky-t); }      .chip--berry { background: var(--berry-t); }
.chip--sun { background: var(--amber-t); }

/* ------------------------------ 12. hero cards --------------------------- */

.herogrid { display: grid; gap: var(--s3); }
.hero-card {
  display: grid; grid-template-columns: auto 1fr auto; grid-template-areas:
    "stage name  badge"
    "stage bar   bar"
    "stage meta  meta";
  align-items: center; column-gap: var(--s3); row-gap: var(--s1);
  padding: var(--s4);
  background: var(--surface); border-radius: var(--r-lg);
  box-shadow: var(--sh-2);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.hero-card:hover { transform: translateY(-3px); box-shadow: var(--sh-3); }
.hero-card__stage {
  grid-area: stage; position: relative; display: grid; place-items: center;
  width: 64px; height: 64px; border-radius: var(--r-md); font-size: var(--fs-display);
}
.hero-card__avatar {
  position: absolute; right: -8px; bottom: -8px;
  display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: var(--r-pill);
  background: var(--surface); box-shadow: var(--sh-1);
  font-size: 17px; line-height: 1;
}
.hero-card__name { grid-area: name; font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title); }
/* R7: имя героя под именем ребёнка. Одна строка и только когда оно есть —
 * без него карточка остаётся прежней высоты. */
.hero-card__heroname {
  display: block; font-family: var(--font-body); font-weight: 600;
  font-size: var(--fs-caption); color: var(--ink-muted);
}
.hero-card .badge { grid-area: badge; }
.hero-card .xpbar { grid-area: bar; }
.hero-card__meta { grid-area: meta; font-size: var(--fs-caption); color: var(--ink-muted); }
.hero-card__alert { color: var(--berry); }
.hero-card--green .hero-card__stage { background: var(--green-t); }
.hero-card--coral .hero-card__stage { background: var(--coral-t); }
.hero-card--sky .hero-card__stage { background: var(--sky-t); }
.hero-card--berry .hero-card__stage { background: var(--berry-t); }
.hero-card--sun .hero-card__stage { background: var(--amber-t); }

.badge {
  display: inline-flex; align-items: center;
  min-height: 32px; padding: 2px 12px; border-radius: var(--r-pill);
  background: var(--surface); box-shadow: var(--sh-1), inset 0 0 0 3px var(--amber);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-caption);
}

/* -------------------------------- 13. xp bar ----------------------------- */

.xpbar {
  position: relative;
  height: 14px; border-radius: var(--r-pill);
  background: rgba(43, 33, 24, .10);
  overflow: hidden;
}
.xpbar__fill {
  width: var(--p, 0%);
  height: 100%;
  background: linear-gradient(90deg, var(--amber), var(--coral));
  border-radius: var(--r-pill);
  transition: width .6s var(--ease-out);
}
.xpbar__fill::after {
  content: "";
  position: absolute; top: 2px; right: 6px;
  width: 24px; height: 40%;
  background: rgba(255, 255, 255, .35);
  border-radius: var(--r-pill);
}
.xpbar--compact { height: 10px; }
.xpbar__label {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-size: 9px; font-weight: 800; letter-spacing: .04em;
  color: var(--ink);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .6);
}

/* ------------------------------ 14. task rows ---------------------------- */

.tasklist { display: grid; gap: var(--s2); }
.taskrow {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  background: var(--surface); border-radius: var(--r-md);
  box-shadow: var(--sh-1);
}
.taskrow__emoji { font-size: var(--fs-title); width: 32px; text-align: center; }
.taskrow__body { flex: 1 1 auto; display: grid; gap: 2px; }
.taskrow__title { font-weight: 700; }
.taskrow__meta { font-size: var(--fs-caption); color: var(--ink-muted); }
/* Действия — своей строкой на всю ширину, как у карточки на подтверждении:
 * перенос «как получится» складывал кнопки у соседних заданий по-разному —
 * у одного «Зачёл» и «Вернуть» в строку, у соседа в две, — и палец искал
 * кнопку заново на каждой карточке. Название при этом получает всю ширину
 * ряда, и «каждый день · 15 XP» больше не рвётся на две строки. */
.taskrow__actions { flex: 1 0 100%; display: flex; gap: var(--s2); align-items: center; }
.taskrow__actions form { flex: 1 1 0; display: flex; }
.taskrow__actions .btn { width: 100%; white-space: nowrap; }
.taskrow__done { font-family: var(--font-display); font-weight: 800; color: var(--success); }
/* У зачтённого действий нет — одна тихая сумма остаётся в строке с названием,
 * а не уезжает на свою: это история, а не кнопка. */
.taskrow--confirmed .taskrow__actions { flex: 0 1 auto; margin-left: auto; }
/* Зачтённое не приглушается прозрачностью: .55 поверх бумаги давало серые
 * буквы ниже контраста. Достаточно зелёной заливки — она и говорит «готово». */
.taskrow--confirmed { background: var(--green-t); box-shadow: none; }
.taskrow--done_by_child { box-shadow: var(--sh-2), inset 0 0 0 2px var(--sky); }
.taskrow__edit {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: var(--fs-caption); font-weight: 700; color: var(--ink-muted);
  text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 2px;
  transition: color var(--dur-fast) var(--ease-out);
}
.taskrow__edit:hover { color: var(--ink); }

/* HABIT: заметка «привычка 🎉» в строке меты. Размер и цвет строки, но тёплый
 * зелёный: это не состояние задания (их читает мета), а похвала, которую
 * родитель видит краем глаза каждый день, пока перечитывает одно и то же. */
.taskrow__habit { color: var(--green); font-weight: 700; }

/* HABIT: панель подтверждения — своя строка на всю ширину ряда, как действия.
 * Решение убрать дело из списка ребёнка читается отдельно от списка, а не
 * внутри него: у панели свой фон и рамка, и глаз находит вопрос, а не кнопку.
 * Кнопка и отказ в строку — оба на всю ширину, пальцу не надо целиться. */
.habitask {
  flex: 1 0 100%; display: grid; gap: var(--s2);
  margin-top: var(--s2); padding: var(--s3);
  background: var(--green-t); border: 2px solid var(--green-d); border-radius: var(--r-md);
}
.habitask__title { font-weight: 700; }
.habitask__hint { font-size: var(--fs-caption); color: var(--ink-muted); }
.habitask .btn { width: 100%; }
.habitask .taskrow__edit { justify-self: start; }

/* ------------- 14c. TASKSCHED1: расписание и план на неделю --------------- */

/* Дни недели — ряд чипов. Нативный чекбокс остаётся в разметке (форма работает
 * без сценария, как вся родительская страница), его не видит никто, кроме
 * клавиатуры: фокус и выбор обводят сам чип. */
.days { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 6px; }
.days__day { position: relative; display: block; cursor: pointer; }
.days__day input { position: absolute; opacity: 0; inset: 0; }
.days__face {
  display: grid; place-items: center;
  min-height: 44px; border-radius: var(--r-pill);
  background: var(--paper); box-shadow: inset 0 0 0 2px var(--line);
  font-size: var(--fs-caption); font-weight: 700; color: var(--ink-muted);
  transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out);
}
.days__day input:checked + .days__face {
  background: var(--amber-t); color: var(--ink);
  box-shadow: inset 0 0 0 3px var(--amber-p);
  transform: scale(1.04);
}
.days__day input:focus-visible + .days__face { outline: 3px solid var(--ring); outline-offset: 3px; }

/* У блока расписания оба управления стоят в разметке всегда: чужое прячет CSS
 * по виду задания, поэтому смена вида работает без сценария, а набранные дни
 * не теряются. Прятать поле из разметки значило бы терять настройку при каждом
 * переключении «Каждый день» ↔ «Один раз». */
.sched { display: grid; gap: var(--s2); }
.sched--once .sched__daily { display: none; }
.sched--daily .sched__once { display: none; }

/* «Быстро» — правка в строке. Свёрнута по умолчанию: список остаётся списком,
 * а не простынёй форм (урок Update 7); открылась — заняла всю строку ряда,
 * как действия и панель привычки: палец ищет её на том же месте. */
.quick { flex: 1 0 100%; }
.quick__toggle {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: var(--fs-caption); font-weight: 700; color: var(--ink-muted);
  text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 2px;
  transition: color var(--dur-fast) var(--ease-out);
}
.quick__toggle::-webkit-details-marker { display: none; }
.quick__toggle:hover { color: var(--ink); }
.quick__form {
  display: grid; gap: var(--s2);
  margin-top: var(--s2); padding: var(--s3);
  background: var(--surface); border-radius: var(--r-md);
  box-shadow: inset 0 0 0 2px var(--line);
}
.quick__form .btn { width: 100%; }
.quick__form .taskrow__edit { justify-self: start; }

/* -------- 14c. WEEKPLAN-TABS: план на неделю — дни вкладками (Update 19) --- */

/* АРИФМЕТИКА 320px. Семь вкладок обязаны встать в один ряд без прокрутки —
 * полоса дней, которую надо листать, это не вкладки. Колонка страницы даёт
 * 320 − 2×16 = 288px, а семь нажатий по 44px — это 308: не хватает. Поэтому
 * полоса выходит на поля страницы и живёт от края до края экрана:
 *   320 − 2×2 (поля полосы) = 316px на вкладки;
 *   сегодня: слово «Сегодня» в 12px своей дорожки — замер на 09-07 дал
 *   52px (подпись 48 + 2×2px полей), а не расчётные 50;
 *   остальные шесть дней: минимум 44px — 6 × 44 = 264;
 *   52 + 264 = 316 = 316 — запаса НЕТ, и это не ошибка расчёта: 44px —
 *   потолок тач-таргета, меньше не бывает. Прокрутка внутри полосы —
 *   страховка на случай шрифта шире замеренного, а не часть раскладки:
 *   review-weekplan-tabs.mjs держит и ряд, и его отсутствие (инъекция
 *   min-width краснеет).
 * Зазор между вкладками нулевой, потому что его физически нет: границу
 * показывает линия-разделитель, а не пустота. */
.wtabs {
  display: grid;
  grid-template-columns: minmax(50px, max-content) repeat(6, minmax(44px, 1fr));
  margin-inline: calc(var(--page-pad) * -1);
  padding: var(--s2) 2px;
  overflow-x: auto; scrollbar-width: none;
  background: var(--paper-deep);
  border-block: 2px solid var(--line);
}
.wtabs::-webkit-scrollbar { display: none; }
/* Вкладка дня — ссылка на якорь раздела: страница перезагружается к плану.
 * Шапка липнет к верху: 68px (кнопка 44px и по 12px паддинга —
 * см. .taskgroup__title выше) плюс 2px её нижней границы, замер 09-07 дал
 * низ на 70px. Запас 2px: без них якорь прятал заголовок раздела под панелью. */
.section--plan { scroll-margin-top: calc(var(--s8) + var(--s2)); }
.wtabs__tab {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s1); min-height: 48px; padding: var(--s1) 2px;
  font-size: var(--fs-label); font-weight: 800; color: var(--ink-muted);
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.wtabs__tab + .wtabs__tab { border-left: 2px solid color-mix(in srgb, var(--line) 60%, transparent); }
.wtabs__name { white-space: nowrap; }
/* «Сегодня» — семь знаков против двух: ему свой кегль, иначе дорожка съедает
 * соседнюю. Остальные дни читаются тем же цветом, что и весь покой полосы. */
.wtabs__tab--today .wtabs__name { font-size: var(--fs-caption); }
.wtabs__tab--today:not(.wtabs__tab--on) { background: var(--amber-t); }
.wtabs__tab--on {
  background: var(--surface); color: var(--ink); box-shadow: var(--sh-1);
}
.wtabs__tab--on,
.wtabs__tab--on + .wtabs__tab { border-left-color: transparent; }

/* Список выбранного дня. Сегодня подсвечен тёплым, как и раньше: тёплое пятно
 * читается до чтения подписей и делает «сегодняшний список» местом, а не
 * случайным раскрытием. */
.wday {
  margin-top: var(--s2); padding: var(--s3);
  background: var(--surface); border-radius: var(--r-md); box-shadow: var(--sh-1);
}
.wday--today { background: var(--amber-t); }
.wday__head { display: flex; align-items: baseline; gap: var(--s2); }
.wday__title { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body); }
.wday__date { font-size: var(--fs-caption); color: var(--ink-muted); }
.wday__head .count { margin-left: auto; }
.wday__list { display: grid; gap: var(--s2); margin-top: var(--s2); }
.wday__item { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--s2); }
.wday__emoji { font-size: var(--fs-title); }
.wday__task { font-size: var(--fs-caption); font-weight: 700; overflow-wrap: anywhere; }
.wday__empty { margin-top: var(--s2); font-size: var(--fs-caption); color: var(--ink-muted); }

@media (max-width: 639px) {
  /* На телефоне строка дня не держит три колонки: «Изменить» (~72px) вместе с
   * эмодзи съедают название, и overflow-wrap: anywhere рвёт его по букве —
   * «Убр ать игр ушки» (замер 09-07 на 358px). Пункт двухколоночный, правка
   * уезжает под название той же строки. */
  .wday__item { grid-template-columns: auto 1fr; }
  .wday__item .taskrow__edit { grid-column: 2; justify-self: start; }
}

@media (min-width: 640px) {
  /* На широкой колонке полоса возвращается внутрь полей и снова становится
   * «предметом» на бумаге, а не линейкой от края до края. */
  .wtabs {
    margin-inline: 0; padding: var(--s1);
    grid-template-columns: repeat(7, 1fr);
    border: 2px solid color-mix(in srgb, var(--line) 50%, transparent);
    border-radius: var(--r-md); overflow: hidden;
  }
  .wday { padding: var(--s4); }
  /* С 640px «Быстро» уходит вправо, к «Изменить», и форма открытия держит
   * свою ширину: гриды внутри флекс-элемента без ширины сжимаются в узкую
   * колонку, и читать правку в строке становится нечем. */
  .quick { flex: 0 1 auto; margin-left: auto; }
  .quick__form { width: 300px; }
}

/* ------------- 14b. группы списка и страница правки (Update 7) ------------ */

/* Подзаголовок группы прилипает под верхней панелью (она 68px: кнопка 44px и
 * по 12px паддинга), чтобы на длинном списке было видно, в какой группе вы.
 * Фон — бумага с лёгким размывом: сквозь него читаются проезжающие ряды. */
.taskgroup__title {
  position: sticky; top: 68px; z-index: var(--z-card);
  display: flex; align-items: center; gap: var(--s2);
  margin: var(--s4) 0 var(--s2);
  padding: var(--s1) var(--s3);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(6px);
  font-size: var(--fs-label); font-weight: 800;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-muted);
}
.taskgroup__title--waiting { color: var(--berry-p); }
.count--quiet { background: var(--paper-deep); color: var(--ink-muted); }
.section__hint { margin: calc(var(--s3) * -1) 0 var(--s3); font-size: var(--fs-caption); color: var(--ink-muted); }

/* UX4: истории на главной карточке больше нет — одна тихая ссылка в хвосте
 * страницы, туда, где родитель ищет не «что сделать сегодня», а «что было». */
.historylink {
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: 44px; padding: 0 var(--s4);
  border-radius: var(--r-pill); background: var(--paper-deep);
  font-size: var(--fs-caption); font-weight: 800; color: var(--ink-muted);
}
.historylink:hover { color: var(--ink); }

/* Журнал не бесконечен: страницы по 60 строк, края подписаны словами
 * «Новее»/«Старее», потому что история — это движение по дням, а не номера. */
.pager {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s4);
}
.pager__link {
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 0 var(--s4);
  border-radius: var(--r-pill); background: var(--paper-deep);
  font-size: var(--fs-caption); font-weight: 800; color: var(--ink-muted);
}
.pager__link:hover { color: var(--ink); }
.pager__count { font-size: var(--fs-caption); color: var(--ink-muted); }

/* ------------- 14c. вкладки видов заданий (UX3, Update 8) ----------------- */

/* Переключатель — не кнопки и не скрипт: два <a> в одном ряду. Подложка —
 * бумага поглубже, чтобы «лоток» читался как один предмет, а активная
 * вкладка поднимается белой карточкой с собственной тенью: состояние видно
 * и без цвета, что и родителю, и тому, кто не различает оттенков. */
.tasktabs {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--s1);
  margin: var(--s3) 0 var(--s4);
  padding: var(--s1);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--paper-deep) 75%, transparent);
}
.tasktabs__tab {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 44px; padding: 0 var(--s2);
  border-radius: var(--r-pill);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-label);
  color: var(--ink-muted); text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.tasktabs__tab:hover { color: var(--ink); }
.tasktabs__tab--active {
  background: var(--surface); color: var(--ink);
  box-shadow: var(--sh-1);
}
.tasktabs__count {
  display: inline-block; min-width: 24px; padding: 1px var(--s1);
  border-radius: var(--r-pill); background: var(--paper-deep);
  font-family: var(--font-body); font-size: var(--fs-caption); font-weight: 800;
  text-align: center; color: var(--ink-muted);
}
.tasktabs__tab--active .tasktabs__count { background: var(--amber); color: var(--ink); }

/* Два названия и два счётчика обязаны уместиться в один ряд на 320px:
 * вкладка, сложившаяся в две строки, перестаёт быть вкладкой. На узком
 * телефоне счётчик отдаёт минимальную ширину и внутренние поля —
 * высота нажатия остаётся 44px. */
@media (max-width: 380px) {
  .tasktabs__tab { gap: var(--s1); padding: 0 var(--s1); }
  .tasktabs__count { min-width: 20px; padding: 1px 2px; }

  /* На узком ДЕТСКОМ экране полосе из трёх элементов не хватает ширины:
   * «Каждый день» при детском размере шрифта не ложится рядом с фигурой и
   * цифрой, и слово ломается пополам — «Каждый / день». Ломать слово нельзя,
   * уменьшать детский шрифт до родительского тоже. Поэтому вкладка становится
   * ПЛИТКОЙ, как дорожка «Герой · Комната · Игры»: фигура сверху, целое слово
   * под ней, цифра последней. Тому, кто не читает, вкладку показывает фигура,
   * и она здесь становится только крупнее. */
  .shell--kid .tasktabs__tab {
    flex-direction: column; gap: var(--s1);
    padding: var(--s2) var(--s1);
  }
  .shell--kid .tasktabs__icon .miniicon { width: 24px; height: 24px; }
  .shell--kid .tasktabs__label { font-size: var(--fs-caption); white-space: nowrap; }
}

/* Страница правки: заголовок называет, что правится, мета — что с ним сейчас. */
.pagehead { display: grid; gap: var(--s1); margin-top: var(--s5); }
.pagehead__title { display: flex; align-items: baseline; gap: var(--s2); font-size: var(--fs-title); line-height: 1.25; }
.pagehead__meta { font-size: var(--fs-caption); color: var(--ink-muted); }

/* Удаление живёт в подвале страницы правки, а не в каждом ряду списка. */
.dangerzone { display: grid; gap: var(--s2); justify-items: start; }

/* ------------------- 15b. статистика и наблюдения ------------------------- */

.statgrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s2); }
.stat {
  display: grid; gap: 2px; justify-items: center;
  padding: var(--s3); border-radius: var(--r-md);
  background: var(--paper-deep); text-align: center;
}
.stat__value { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title); }
.stat__label { font-size: 10px; color: var(--ink-muted); text-align: center; }

/* AN1: наблюдения. Одна колонка на телефоне — карточка читается как абзац, а
 * не как плитка среди плиток: совет, ради которого блок существует, теряется
 * в сетке. Иконка — подпись к настроению, текст несёт смысл, поэтому она
 * меньше заголовка и не выравнивает всё остальное по себе. */
.insights { display: grid; gap: var(--s2); margin-top: var(--s3); }
.insight {
  display: flex; gap: var(--s3); align-items: flex-start;
  padding: var(--s3) var(--s4);
  background: var(--paper-deep); border-radius: var(--r-md);
}
.insight__icon { flex: none; font-size: var(--fs-title); line-height: 1.1; }
.insight__body { display: grid; gap: 2px; min-width: 0; }
.insight__title { font-weight: 700; }
.insight__text { font-size: var(--fs-caption); color: var(--ink-muted); }
.insight__action {
  font-size: var(--fs-caption); font-weight: 700; color: var(--ink);
  text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 2px;
}
a.insight__action { min-height: 44px; display: inline-flex; align-items: center; }

.childhead {
  display: flex; gap: var(--s4); align-items: flex-start;
  margin-top: var(--s5); padding: var(--s5);
  border-radius: var(--r-xl); background: var(--surface);
  box-shadow: var(--sh-sticker);
}
.childhead__stage {
  display: grid; place-items: center; flex: 0 0 auto;
  width: 88px; height: 88px; border-radius: var(--r-lg); font-size: var(--fs-display);
}
.childhead--green .childhead__stage { background: var(--green-t); }
.childhead--coral .childhead__stage { background: var(--coral-t); }
.childhead--sky .childhead__stage { background: var(--sky-t); }
.childhead--berry .childhead__stage { background: var(--berry-t); }
.childhead--sun .childhead__stage { background: var(--amber-t); }
.childhead__main { flex: 1 1 auto; display: grid; gap: var(--s2); }
.childhead__name { font-size: var(--fs-title); }
.childhead__stage-name { font-size: var(--fs-caption); color: var(--ink-muted); }
.childhead__meta { font-size: var(--fs-caption); color: var(--ink-muted); }
/* GAMES2: уровень растёт и из игр, а журнал дел молчит — строка сверяет
   одно с другим. Появляется только когда за неделю игры правда платили. */
.childhead__games { font-size: var(--fs-caption); color: var(--ink-muted); opacity: .8; }

/* ------------------------------- 15. stickers ---------------------------- */

.stickers { display: grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); gap: var(--s3); }
.sticker {
  position: relative;
  aspect-ratio: 1; display: grid; place-items: center;
  border: 4px solid var(--surface); border-radius: var(--r-md);
  box-shadow: var(--sh-sticker);
  background: var(--paper-deep);
  font-size: var(--fs-title);
  animation: sticker-in var(--dur-slow) var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 45ms);
}
.stickers > .sticker:nth-child(odd) { transform: rotate(-1.2deg); }
.stickers > .sticker:nth-child(even) { transform: rotate(1.1deg); }
.sticker--got { background: var(--amber-t); }
.sticker--locked { background: var(--paper-deep); box-shadow: inset 0 0 0 2px var(--line); }
.sticker--locked .sticker__emoji { opacity: .55; }
.sticker__count {
  position: absolute; right: -6px; bottom: -6px;
  min-width: 22px; height: 22px; padding: 0 5px;
  display: grid; place-items: center;
  background: var(--berry); color: #fff;
  border-radius: var(--r-pill);
  font-size: 11px; font-weight: 800;
}

.log { display: grid; gap: var(--s2); }
.logrow {
  display: flex; align-items: baseline; gap: var(--s2);
  padding: var(--s2) var(--s3);
  font-size: var(--fs-caption);
  background: var(--surface); border-radius: var(--r-sm); box-shadow: var(--sh-1);
}
.logrow__icon { flex: 0 0 auto; }
.logrow__text { flex: 1 1 auto; }
.logrow__time { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
/* ------------------------------ 16. role chooser ------------------------- */

.rolegrid { display: grid; gap: var(--s4); }
.role {
  padding: var(--s5); border-radius: var(--r-xl);
  background: var(--surface); box-shadow: var(--sh-sticker);
  display: grid; gap: var(--s3); justify-items: start;
}
.role--kid { border: 4px solid var(--amber); }
.role--parent { border: 4px solid var(--line); }
.role__title { font-size: var(--fs-title); }
.role__hint { color: var(--ink-muted); font-size: var(--fs-caption); margin-top: calc(var(--s2) * -1); }
.role__grid { display: flex; flex-wrap: wrap; gap: var(--s3); width: 100%; }

.kidgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--s4); padding: var(--s4); }
.kidcard {
  display: grid; justify-items: center; gap: var(--s2);
  padding: var(--s5) var(--s4);
  border-radius: var(--r-xl); background: var(--surface);
  border: 4px solid var(--surface);
  box-shadow: var(--sh-sticker);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-spring);
}
.kidcard:nth-child(odd) { transform: rotate(-1.2deg); }
.kidcard:nth-child(even) { transform: rotate(1.1deg); }
.kidcard:hover { transform: rotate(0) translateY(-4px) scale(1.02); }
.kidcard__emoji { font-size: calc(var(--fs-display) * 1.6); line-height: 1; }
.kidcard__name { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title); }
.kidcard--green { background: var(--green-t); } .kidcard--coral { background: var(--coral-t); }
.kidcard--sky { background: var(--sky-t); }      .kidcard--berry { background: var(--berry-t); }
.kidcard--sun { background: var(--amber-t); }

/* ------------------------------- 17. PIN gate ---------------------------- */
/* SEC1: вход родителя с детского планшета. Раздел писался под этот экран ещё
 * в прототипе и впервые встретил свою страницу здесь. Клавиши 64px — рост с
 * прототипных 56: владелец просил крупные, и набирает их родитель, держа
 * планшет одной рукой. Скрипт (pin.js) переставляет форму из «поле + кнопка»
 * в «точки + клавиатура»; без скрипта всё остаётся полем и кнопкой. */

.shell--gate { padding-bottom: var(--s5); }
.gate { display: grid; gap: var(--s4); justify-items: center; }
.gate__dots { display: flex; gap: var(--s3); min-height: 16px; }
.gate__dot {
  width: 14px; height: 14px; border-radius: var(--r-pill);
  background: rgba(43, 33, 24, .18);
  transition: background-color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-spring);
}
.gate__dot--on { background: var(--ink); transform: scale(1.15); }
.gate__dots.is-wrong .gate__dot { background: var(--coral); }
.gate__dots.is-wrong { animation: shake 380ms var(--ease-out); }
.keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s3); width: 100%; max-width: 280px; }
.key {
  min-height: 64px;
  border-radius: var(--r-md); background: var(--surface);
  box-shadow: var(--sh-1);
  font-family: var(--font-display); font-weight: 700; font-size: var(--fs-title);
  transition: transform var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
}
.key:hover { background: var(--paper-deep); }
.key:active { transform: scale(.96); background: var(--paper-deep); }
.key__glyph { display: block; width: 26px; height: 26px; margin: 0 auto; }
.key--erase { color: var(--ink-muted); }
.key--go { background: var(--amber); color: var(--on-accent); box-shadow: var(--sh-2); }
.key--go:hover { background: var(--amber-h); }
.key--go:active { background: var(--amber-p); }
/* Блокировка троттлингом: текст выше сказал «подождите», клавиши глухи. */
.pinform--locked .keypad { pointer-events: none; opacity: .55; }
.pinform__field { width: min(100%, 240px); }
.pinform__go { width: 100%; }
.pinform:not(.pinform--keyed) .gate__dots,
.pinform:not(.pinform--keyed) .keypad { display: none; }
.pinform--keyed .pinform__field,
.pinform--keyed .pinform__go { display: none; }
.gate__backlink { display: block; text-align: center; margin-top: var(--s5); color: var(--ink-muted); }

/* OAUTH1: соц-вход под формой входа/регистрации. Вторичные действия —
 * призрачные кнопки на всю ширину строки: главное действие экрана всегда
 * пароль (D2), соцсеть — ровно один альтернативный путь, не витрина. */
.oauth-gate { display: grid; gap: var(--s2); margin-top: var(--s5); }
.oauth-gate .footnote { margin: 0; }

/* -------------------------------- 18. kid mode --------------------------- */

.kidhead {
  margin-top: var(--s4); padding: var(--s5);
  border-radius: var(--r-xl); background: var(--surface);
  box-shadow: var(--sh-sticker);
  display: grid; gap: var(--s3);
}
.kidhead__top { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.kidhead__avatar { font-size: var(--fs-display); }
.kidhead__name { font-size: var(--fs-display); }
.kidhead__hero {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3); border-radius: var(--r-lg);
  background: var(--paper-deep);
  /* Строка сетки не умеет сжимать элемент ниже его содержимого: без этого
   * герой + имя стадии + кошелёк держали шапку шириной 358px, и на 320-м
   * телефоне кошелёк, выключатель звука и полоса опыта уезжали за край.
   * min-width: 0 разрешает строке сжиматься, а wrap уводит кошелёк на свою
   * строку, когда на всех троих места нет: сжать имя стадии до нечитаемого
   * хуже, чем перенести. */
  min-width: 0; flex-wrap: wrap;
}
.kidhead__hero-name { display: grid; gap: 2px; min-width: 0; }
.kidhead__stage-name {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-title); line-height: 1.15;
}
.kidhead__stage-motto { color: var(--ink-muted); font-size: var(--fs-caption); line-height: 1.3; }
.kidhead__level { display: flex; align-items: center; gap: var(--s3); }
.kidhead__level .xpbar { flex: 1 1 auto; height: 20px; }
/* XP-цифры — взрослым; ребёнок видит только полосу. */
.kidhead__level .xpbar__label { display: none; }

/* M3: огоньки недели — последняя строка шапки, под полосой опыта. Полоса
 * отвечает «насколько вырос герой», огоньки — «сколько дней ты был героем»:
 * рядом им стоять можно, сливаться нельзя, поэтому огоньки круглые и тёплые,
 * а полоса осталась длинной. Кнопка целиком: тап в любом месте объясняет
 * голосом, значок динамика подсказывает, что здесь есть звук.
 *
 * D5: уехав из сетки шапки в комнату, строка потеряла родителя, ради
 * которого жили отрицательные поля и justify-self, — и прилипла к банке
 * недели (зазор 0 на 358px, измерено). Теперь это просто тихая полоса
 * комнаты: содержательной ширины, с общим ритмом вертикали. */
.kidweek {
  display: inline-flex; align-items: center; gap: var(--s2);
  /* Пол тапа: ряд огоньков тапается (говорит, что огоньки значат),
   * 31px высотой — это был не тап, а попадание. */
  min-height: 44px; padding: var(--s2) var(--s3);
  margin-top: var(--s5);
  border: 0; background: none; cursor: pointer;
  border-radius: var(--r-pill);
  transition: transform var(--dur-fast) var(--ease-spring);
}
.kidweek:active { transform: scale(.97); }
.kidweek__icon { flex: 0 0 auto; font-size: 15px; color: var(--ink-faint); }
.kidweek__row { display: flex; align-items: center; gap: 7px; }
.kidweek__light {
  display: grid; place-items: center;
  width: 15px; height: 15px; border-radius: var(--r-pill);
  /* Погасший огонёк — не дырка, а тот же огонёк во сне: та же форма и размер,
   * только без света. Никакого крестика и никакого красного — ряд не показывает
   * ребёнку потерю, потому что потерянного и не было. */
  background: var(--paper-deep);
  box-shadow: inset 0 0 0 2px rgba(43, 33, 24, .06);
}
.kidweek__light--lit {
  background: linear-gradient(160deg, var(--amber-h), var(--coral));
  box-shadow: none;
}
.kidweek__light--rest {
  background: var(--amber-t);
  box-shadow: inset 0 0 0 2px rgba(240, 179, 31, .45);
}
/* Отдых — часть правила, а не провал: он искрой, а не пустым местом, поэтому
 * неделя с выходным выглядит прожитой, а не испорченной. */
.kidweek__spark { width: 9px; height: 9px; fill: var(--amber-p); }
/* Сегодняшний огонёк ждёт своего дела. Ободок — постоянный, дышит только
 * сердцевина (opacity, а не геометрия): это приглашение, а не тревога. */
.kidweek__light--wait {
  background: var(--amber-t);
  box-shadow: inset 0 0 0 2px var(--amber);
}
.kidweek__light--wait::after {
  content: ''; width: 5px; height: 5px; border-radius: var(--r-pill);
  background: var(--amber-p); opacity: .8;
  animation: kidweek-breathe 3.2s var(--ease-out) infinite;
}
@keyframes kidweek-breathe {
  0%, 100% { opacity: .35; }
  50% { opacity: 1; }
}
/* Глобальный рубильник reduced-motion гасит дыхание; сердцевина остаётся
 * видимой — ободок и так говорит «это сегодня». */

/* R7: имя героя. Кнопка, а не надпись: двухлетний читает ухом, и тап по имени
 * отвечает «Меня зовут Рыжик!». Рисована как заголовок стадии, который стоял
 * здесь до неё, — появление имени не должно перерисовывать шапку, — но со
 * значком динамика: кнопка, которая выглядит как текст, ребёнок не нажмёт. */
.heroname {
  display: inline-flex; align-items: center; gap: var(--s2);
  justify-self: start; min-width: 0; max-width: 100%;
  padding: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-title); line-height: 1.15; color: var(--ink);
  text-align: left;
  transition: color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-spring);
}
/* Имя не переносится и не обрезает шапку: длинное имя сжимается в многоточие,
 * а целиком его видно родителю, который это имя вводил. */
.heroname__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.heroname__icon { flex: 0 0 auto; font-size: 16px; color: var(--ink-muted); }
.heroname:active { transform: scale(.97); }
.heroname.is-speaking { color: var(--green); }
.heroname.is-speaking .heroname__icon { color: var(--green); }

.kidsection { display: grid; gap: var(--s3); margin-top: var(--s4); }
.kidsection .kidtask:nth-child(odd) { transform: rotate(-1.2deg); }
.kidsection .kidtask:nth-child(even) { transform: rotate(1.1deg); }
/* Update 7 / D1: карточка сжалась с 250–315px до ~140px, но не стала тише —
 * тот же стикер, тот же наклон, тот же празднование. Дело в строку: картинка,
 * название и награда рядом, кнопка — во всю ширину под ними. Двадцать заданий
 * больше не превращают экран в простыню, а палец по-прежнему не целится. */
.kidtask {
  position: relative;
  display: grid; gap: var(--s3); text-align: left;
  padding: var(--s4);
  background: var(--surface);
  border: 4px solid var(--surface); border-radius: var(--r-lg);
  box-shadow: var(--sh-sticker);
  animation: sticker-in var(--dur-slow) var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 45ms);
  transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out);
}
.kidtask:hover { transform: rotate(0) translateY(-3px); box-shadow: var(--sh-3); }
/* Динамик в углу занимает верхние 44px справа: строка в них не заходит, чтобы
 * эмодзи и награда не оказались под кнопкой (см. цикл 7, 320px).
 *
 * Названию нужна своя ширина, а не остаток. Строка была `nowrap`: эмодзи (66px),
 * награда (59px), два промежутка и резерв под динамик съедали 233px из 275px
 * карточки, и названию доставалось 40px — столбик в два знака. Слово шире
 * своего ящика не переносится, а рисуется поверх: на 320px «зарядку» лежало
 * под янтарной ценой, и проверка прямоугольников этого не видела — ящик-то
 * узкий, вылезает уже нарисованный текст. Теперь строке разрешено ломаться, а
 * у названия есть минимум: когда в остаток слово не помещается, цена уходит со
 * строки и название забирает всю ширину. Случайное слово длиннее любой ширины
 * рвётся, а не рисуется под ценой. */
.kidtask__head {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); min-width: 0;
  padding-right: 44px;
}
.kidtask__emoji {
  font-size: clamp(40px, 14vw, calc(var(--fs-display) * 1.3)); line-height: 1; flex: none;
}
.kidtask__title {
  font-size: var(--fs-title); line-height: 1.2;
  /* 116px, а не «сколько осталось»: минимум обязан быть больше, чем доля
   * названия, при которой цена остаётся в строке на 390px, и меньше, чем
   * доля, при которой название ещё уживается с картинкой на 320px, — иначе
   * на широком телефоне цена встанет в строку и столбик вернётся, а на
   * узком название уйдёт на отдельную строку от картинки. */
  flex: 1 1 0; min-width: 116px; overflow-wrap: break-word;
}
.kidtask__reward {
  flex: none; margin-left: auto;
  font-size: var(--fs-caption); font-weight: 800;
  padding: 2px 12px; border-radius: var(--r-pill);
  background: var(--amber-t);
  transition: background-color var(--dur-base) var(--ease-out);
}
/* M1: пока задание «Проверяем…», обещанная цена висит пунктиром — она ещё не
 * в копилке, и ребёнок видит это без единого слова. Цифра остаётся той же:
 * смена числа читалась бы как «отняли». */
.kidtask--done_by_child .kidtask__reward {
  background: transparent;
  border: 2px dashed var(--amber-p);
  color: var(--ink-muted);
}
.kidtask__form { width: 100%; }
.kidtask__btn {
  width: 100%; min-height: 64px; border-radius: var(--r-pill);
  background: var(--coral); color: var(--on-accent);
  box-shadow: var(--sh-sticker);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title);
  transition: transform var(--dur-fast) var(--ease-spring), background-color var(--dur-fast) var(--ease-out);
}
.kidtask__btn::after { content: "Готово! 👍"; }
.kidtask.is-arming .kidtask__btn { background: var(--green); }
.kidtask.is-arming .kidtask__btn::after { content: "Точно готово? ✅"; }
.kidtask.is-arming { animation: pop var(--dur-pop) var(--ease-spring); }
.kidtask__btn:active { transform: scale(.97); }
.kidtask__wait {
  font-size: var(--fs-label); font-weight: 700; color: var(--ink);
  background: var(--sky-t); padding: var(--s2) var(--s4);
  border-radius: var(--r-pill); width: 100%; text-align: center;
}
.kidtask__done {
  font-size: var(--fs-label); font-weight: 700; color: var(--surface);
  background: var(--green); padding: var(--s2) var(--s4);
  border-radius: var(--r-pill); width: 100%; text-align: center;
}
/* Все три состояния нарисованы в разметке всегда, показывает класс: M1
 * переворачивает карточку на клиенте, а при следующем опросе она обязана
 * собраться обратно в любое из трёх — в том числе назад, в «Готово!». */
.kidtask:not(.kidtask--assigned) .kidtask__form { display: none; }
.kidtask:not(.kidtask--done_by_child) .kidtask__wait { display: none; }
.kidtask:not(.kidtask--confirmed) .kidtask__done { display: none; }
.kidtask--done_by_child { border-color: var(--sky); }
.kidtask--confirmed { background: var(--green-t); border-color: var(--green-d); }
.kidtask--confirmed .kidtask__done { background: var(--green-p); }

/* M1: предпраздник. Он живёт НА КАРТОЧКЕ, а не во весь экран: экранное
 * торжество — ответ за настоящее дело и за чужое решение, и если тот же
 * занавес открывается по первому тапу, тап сам становится игрой, а зачёт —
 * шумом. Здесь — короткий прыжок карточки и подскок цены: временный опыт —
 * это та же цифра, что и всегда, только пунктиром, потому что дело ещё не
 * зачтено. Второй, летающей копии числа нет: на 358px она ложится на название
 * дела и отнимает у него место. */
.kidtask.is-celebrating { animation: kidtask-cheer var(--dur-pop) var(--ease-spring); }
/* Прыжок выпрямляет карточку, как делает и наведение: перекос — знак «это
 * просто карточка в списке», праздник — знак «вот это дело». */
@keyframes kidtask-cheer {
  0% { transform: scale(1) rotate(0deg); }
  40% { transform: scale(1.04) rotate(0deg); }
  100% { transform: scale(1) rotate(0deg); }
}
.kidtask.is-celebrating .kidtask__reward { animation: reward-pop var(--dur-pop) var(--ease-spring); }
@keyframes reward-pop {
  0% { transform: scale(1); }
  40% { transform: scale(1.3); }
  100% { transform: scale(1); }
}
/* Возврат задания (родитель нажал «Вернуть»). Голос молчит и цвет не красный:
 * ребёнок ничего не потерял — дело просто ещё не сделано, и кнопку можно
 * нажать снова. Уходит только голубая краска «проверяем». */
.kidtask.is-returned { animation: kidtask-settle .6s var(--ease-out); }
@keyframes kidtask-settle {
  from { background: var(--sky-t); border-color: var(--sky); }
  to { background: var(--surface); border-color: var(--surface); }
}

/* Update 7 / D2: три дорожки под заданиями стали одним рядом из трёх плиток.
 * Плитка — карточка с подвалом-кнопкой звука: подвал прирос к ней снизу, чтобы
 * «пойти» и «послушать» читались как одно целое, а не шесть отдельных целей. */
.kiddest {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s2); margin-top: var(--s4);
}
.kiddest__cell { position: relative; display: grid; }
.kiddest__link {
  display: grid; justify-items: center; gap: var(--s2);
  padding: var(--s3) var(--s2);
  background: var(--surface); border: 4px solid var(--surface);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: var(--sh-sticker);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-spring);
}
.kiddest__link:active { transform: scale(.97); }
.kiddest__figure {
  display: grid; place-items: center;
  width: 72px; height: 72px; border-radius: var(--r-md);
  background: var(--paper-deep); font-size: 34px; line-height: 1;
}
.kiddest__figure .hero { --hero-size: 60px; }
.kiddest__label { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-caption); }
.kiddest__cell .say {
  position: static; width: 100%; height: 44px;
  border: 0; border-radius: 0 0 var(--r-lg) var(--r-lg);
  font-size: 20px;
  box-shadow: var(--sh-sticker);
  /* Светлее, чем у динамиков на заданиях: плитка — соседняя дорога, а не
   * главное действие экрана, и три сплошных янтарных полосы звали громче
   * коралловой кнопки «Готово». Зелёный отклик при звучании остаётся. */
  background: var(--amber-d);
}

/* ------------------------- 12b. голос героя (R1) ------------------------- */
/* Кнопка-стикер: ребёнок может не читать, но «нажми на динамик» он понимает.
 * Пока звучит — кнопка зеленеет и пускает волны, чтобы было видно, откуда
 * звук. Волны — анимация, зелёный — цвет, поэтому состояние видно и при
 * prefers-reduced-motion. */
.say {
  position: absolute; top: calc(var(--s2) * -1); right: calc(var(--s2) * -1);
  width: 56px; height: 56px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--amber); color: var(--ink);
  border: 3px solid var(--surface);
  box-shadow: var(--sh-sticker);
  font-size: 26px; line-height: 1;
  transition: transform var(--dur-fast) var(--ease-spring), background-color var(--dur-fast) var(--ease-out);
}

/* Значок динамика. Размер в em, чтобы 26px у .say/.sayword и 20px у
 * .voice-toggle продолжали задавать его, как задавал эмодзи. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.icon-say {
  display: block; width: 1em; height: 1em;
  fill: currentColor; /* волны и черта задают stroke сами, в спрайте */
}
.voice-toggle .icon-say__off { display: none; }
.voice-toggle.is-muted .icon-say__on { display: none; }
.voice-toggle.is-muted .icon-say__off { display: block; }
.say:active { transform: scale(.92); }
.say.is-speaking { background: var(--green); color: var(--surface); }
.say.is-speaking::before,
.say.is-speaking::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  border: 3px solid var(--green); opacity: 0;
  animation: say-ring 1.5s var(--ease-out) infinite;
}
.say.is-speaking::after { animation-delay: .75s; }
@keyframes say-ring {
  from { transform: scale(1); opacity: .7; }
  to { transform: scale(1.9); opacity: 0; }
}

/* Выключатель звука в шапке — для родителя, когда в доме уже поздно. */
.voice-toggle {
  /* Тап-таргет не участвует в спорах за ширину (прецедент gamehead,
   * цикл 50): усаживался 44→36 на хабе и доме — ужинается текст рядом,
   * а не палец. */
  flex: 0 0 auto;
  margin-left: auto; width: 44px; height: 44px; border-radius: var(--r-pill);
  display: grid; place-items: center; font-size: 20px; line-height: 1;
  background: rgba(255, 255, 255, .55); color: var(--ink);
  border: 2px solid var(--surface);
  box-shadow: var(--sh-sticker);
  transition: transform var(--dur-fast) var(--ease-spring), opacity var(--dur-fast) var(--ease-out);
}
.voice-toggle:active { transform: scale(.92); }
.voice-toggle.is-muted { opacity: .55; }

/* ------------------------ 12c. выбор голоса (R8) ------------------------ */
/* Строки-стикеры в «Настройках» родителя. Радио — невидимое, как у .pick:
 * выбирает вся строка, а не маленький кружок, в который не попадёшь пальцем.
 * Проба — отдельная кнопка справа, поэтому «послушать» и «выбрать» — два
 * разных действия, и случайный тап по динамику не меняет голос у ребёнка. */
.voices { display: grid; gap: var(--s3); }
.voices__list { display: grid; gap: var(--s2); }
.voice { position: relative; display: block; cursor: pointer; }
.voice__input { position: absolute; opacity: 0; inset: 0; margin: 0; }
.voice__face {
  display: grid; gap: 2px;
  padding: var(--s2) calc(var(--s5) + var(--s2)) var(--s2) var(--s3);
  border-radius: var(--r-md);
  background: var(--paper);
  box-shadow: inset 0 0 0 2px var(--line);
  transition: box-shadow var(--dur-fast) var(--ease-out);
}
.voice input:checked + .voice__face {
  box-shadow: inset 0 0 0 3px var(--ink);
  background: var(--amber-t);
}
.voice input:focus-visible + .voice__face { outline: 3px solid var(--ring); outline-offset: 3px; }
.voice__kind { font-size: var(--fs-body); font-weight: 800; color: var(--ink); }
.voice__def { font-size: var(--fs-caption); font-weight: 600; color: var(--ink-muted); }
.voice__tone { font-size: var(--fs-caption); color: var(--ink-muted); }
/* Проба живёт в строке, а не на её углу: .say по умолчанию нависает над краем
 * карточки, а здесь пять строк подряд и нависание цепляло бы соседнюю. 48px —
 * палец попадает, ряд остаётся ровным. */
.say--row {
  top: 50%; right: var(--s2); transform: translateY(-50%);
  width: 48px; height: 48px; border-width: 2px; font-size: 22px;
}
.say--row:active { transform: translateY(-50%) scale(.92); }

/* Подсказка в родительском списке: то, что герой читает вслух. */
.taskrow__hint {
  font-size: var(--fs-caption); color: var(--ink-muted);
  display: flex; align-items: center; gap: var(--s1);
}
.taskrow__hinticon { flex: none; display: block; width: 14px; height: 14px; }
.taskrow__hinticon .icon-say { width: 100%; height: 100%; }
.field__optional { font-weight: 500; color: var(--ink-muted); }

.kidhint {
  margin-top: var(--s4); padding: var(--s3) var(--s4);
  border-radius: var(--r-md); background: var(--sky-t);
  font-size: var(--fs-body); text-align: center;
}

/* ======================= ALBUM1: альбом наклеек ===========================
 * Книга, а не полка: листы в переплёте, васи-лента держит лист, наклейки
 * стоят с лёгким наклоном, как настоящие. Неоткрытое место — пунктирный
 * силуэт той самой наклейки: замок с цифрой говорил бы «нельзя», силуэт
 * говорит «приедет». Всё по мокапу владельца (design/screen-album.html). */

/* --- вход с комнаты: карточка-книжка вместо инлайн-сетки ----------------- */
.albumcard {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s5); padding: var(--s3) var(--s4);
  border-radius: var(--r-md); background: var(--surface);
  box-shadow: var(--sh-sticker); text-decoration: none;
}
.albumcard__art { display: flex; flex: none; }
.albumcard__art .stk { width: 40px; height: 40px; }
.albumcard__art .stk + .stk { margin-left: -14px; }
.albumcard__art .stk:nth-child(1) { transform: rotate(-6deg); }
.albumcard__art .stk:nth-child(3) { transform: rotate(7deg); }
.albumcard__text { display: grid; flex: 1 1 auto; min-width: 0; }
.albumcard__name { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-small); }
.albumcard__count { font-size: var(--fs-caption); color: var(--ink-muted); }
.albumcard__go { font-family: var(--font-display); font-size: var(--fs-title); color: var(--ink-faint); }

/* --- рисованная наклейка и её силуэт ------------------------------------- */
.stk { display: block; width: 100%; height: auto; }
.stk-emoji {
  display: grid; place-items: center; width: 100%; aspect-ratio: 1;
  border-radius: 50%; background: var(--surface); box-shadow: var(--sh-sticker);
  font-size: var(--fs-title); line-height: 1;
}
.stk-sil { display: block; width: 100%; height: auto; opacity: .95; }

/* --- лист альбома -------------------------------------------------------- */
.albumsheet { position: relative; margin-top: var(--s6); }
.albumsheet__paper {
  position: relative;
  padding: var(--s4) var(--s3) var(--s4);
  border: 2px solid var(--line); border-radius: var(--r-md);
  background:
    radial-gradient(circle at 12% 8%, rgba(255, 201, 60, .07) 0 26%, transparent 27%),
    radial-gradient(circle at 88% 22%, rgba(79, 163, 209, .08) 0 22%, transparent 23%),
    radial-gradient(circle at 78% 86%, rgba(255, 107, 74, .06) 0 24%, transparent 25%),
    var(--surface);
}
.washi {
  position: absolute; top: -12px; width: 72px; height: 24px; opacity: .9; z-index: 2;
  background: repeating-linear-gradient(45deg, rgba(255, 255, 255, .35) 0 6px, transparent 6px 12px), var(--amber-t);
  box-shadow: 0 1px 3px rgba(43, 33, 24, .10);
}
.washi--left { left: 18px; transform: rotate(-5deg); }
.washi--right { right: 18px; transform: rotate(4deg); background: repeating-linear-gradient(-45deg, rgba(255, 255, 255, .4) 0 6px, transparent 6px 12px), var(--sky-t); }

.albumsheet__head { display: flex; align-items: center; gap: var(--s2); margin-bottom: var(--s3); }
.albumsheet__icon { flex: none; width: 34px; }
.albumsheet__moon { flex: none; width: 34px; height: 34px; }
.albumsheet__name {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-small); line-height: 1.2; text-align: left;
}
/* Имя набора — она же кнопка: тап называет набор вслух (R1), поэтому тап-таргет
 * честные 44px, а не высота строки заголовка. */
.albumsheet__say {
  display: inline-flex; align-items: center; gap: var(--s1); text-align: left;
  min-height: 44px; margin: calc(var(--s2) * -1) 0;
}
.albumsheet__say .icon-say { width: 20px; height: 20px; flex: none; }
.albumsheet__count {
  flex: none; padding: 2px var(--s2); border-radius: var(--r-pill);
  background: var(--paper-deep);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-caption);
}
.albumsheet__count--done { background: var(--green-t); color: var(--green-p); }
.albumsheet__sleep { display: inline-flex; flex: none; color: var(--sky-p); font-style: italic; font-weight: 800; }
.albumsheet__sleep i { font-family: var(--font-display); font-size: var(--fs-caption); line-height: 1; }
.albumsheet__sleep i + i { margin-left: 1px; font-size: var(--fs-small); }
.albumsheet__sleep i + i + i { font-size: var(--fs-body); }

.albumsheet__grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s3);
}
@media (min-width: 640px) {
  .albumsheet__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* Ячейка: наклон и сдвиг — по ключу наклейки, один и тот же при каждом входе,
 * чтобы альбом не «переезжал» под глазами. Тап по своей наклейке называет её. */
.slot { position: relative; transform: rotate(var(--rot, 0deg)) translateY(var(--dy, 0px)); }
.slot .stk { filter: drop-shadow(0 2px 0 rgba(43, 33, 24, .05)) drop-shadow(0 8px 14px rgba(43, 33, 24, .13)); }
.slot:active .stk { transform: scale(.88) rotate(-3deg); }
.slot__count {
  position: absolute; right: -4px; bottom: -2px;
  min-width: 22px; padding: 0 5px; border-radius: var(--r-pill);
  border: 2px solid var(--surface); background: var(--amber);
  font-family: var(--font-display); font-weight: 800; font-size: 11px; line-height: 18px;
}
.slot--locked { cursor: default; }

/* --- футер листа: обещание или покупка ----------------------------------- */
.albumsheet__footer {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  margin-top: var(--s3); padding: var(--s2) var(--s3);
  border-radius: var(--r-md); background: var(--paper);
}
.albumsheet__note { font-size: var(--fs-caption); color: var(--ink-muted); text-align: left; }
.albumsheet__footer--done { background: var(--green-t); }
.albumsheet__footer--done .albumsheet__note { color: var(--green-p); font-weight: 700; }
.albumsheet__footer--bought { background: var(--amber-t); }

.albumbuy { display: flex; align-items: center; gap: var(--s2); margin: 0; flex: 1 1 auto; }
.albumbuy__text { display: grid; flex: 1 1 auto; min-width: 0; }
.albumbuy__title { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-caption); }
.albumbuy__hint { font-size: 11px; color: var(--ink-muted); }
.albumbuy__btn {
  display: inline-flex; align-items: center; gap: 3px; flex: none;
  padding: var(--s1) var(--s3); border-radius: var(--r-pill);
  background: var(--amber); box-shadow: var(--sh-1);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-caption);
}
.albumbuy__btn .coin { width: 16px; height: 16px; }
/* Двухтакт: цена уступает место вопросу, тот же жест, что «Купить» →
 * «Точно купить?» в магазине. Вопрос произносит голос, на кнопке — коротко. */
.albumbuy__q { display: none; }
.albumbuy.is-arming .albumbuy__btn { background: var(--coral); }
.albumbuy.is-arming .albumbuy__n,
.albumbuy.is-arming .albumbuy__btn .coin { display: none; }
.albumbuy.is-arming .albumbuy__q { display: inline; }
.albumbuy__short {
  flex: none; padding: var(--s1) var(--s2); border-radius: var(--r-pill);
  background: var(--paper-deep); white-space: nowrap;
  font-family: var(--font-display); font-weight: 800; font-size: 11px;
}
.albumbuy__go {
  flex: none; padding: var(--s1) var(--s3); border-radius: var(--r-pill);
  background: var(--amber); font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-caption);
}

/* --- собранный лист: гирлянда и печать ----------------------------------- */
.albumsheet--done .albumsheet__paper { border-color: var(--green-d); }
.albumsheet__garland { display: block; width: 100%; height: auto; margin-top: var(--s3); }
.albumstamp {
  display: inline-grid; justify-items: center; flex: none;
  width: 62px; height: 62px; padding-top: 4px; border-radius: 50%;
  border: 2.5px solid var(--green-p); color: var(--green-p);
  transform: rotate(-8deg);
  font-family: var(--font-display); font-weight: 800; font-size: 11px; line-height: 1.1;
}
.albumstamp .stk { width: 22px; }

/* --- спящий лист --------------------------------------------------------- */
.albumsheet--sleeping .albumsheet__paper {
  background:
    radial-gradient(circle at 82% 10%, rgba(79, 163, 209, .10) 0 20%, transparent 21%),
    linear-gradient(180deg, var(--paper) 0%, var(--paper-deep) 100%);
}
.albumsheet--sleeping .stk-sil { opacity: .7; }
.albumsheet--sleeping .albumsheet__footer { background: transparent; }

/* --- полоса идущего праздника -------------------------------------------- */
.albumfest {
  display: flex; align-items: center; gap: var(--s2);
  margin-top: var(--s4); padding: var(--s2) var(--s3);
  border-radius: var(--r-md); background: var(--amber-t);
  font-size: var(--fs-caption);
}
.albumfest__icon { flex: none; width: 26px; }
.albumfest__say { flex: none; width: 18px; color: var(--ink-muted); }

/* --- шапка альбома ------------------------------------------------------- */
/* 36px, как .voice-toggle в этой же шапке: прецедент gamehead — в заголовке
 * тап-таргет ужинается, потому что рядом текст, а не палец. */
.albumhead__say { display: inline-flex; width: 36px; height: 36px; vertical-align: middle; color: var(--ink-muted); }

/* Свежая наклейка «приклеивается» на глазах: одно короткое движение, которое
 * читается как событие, а не как украшение. Поэтому оно одно во всём альбоме
 * переживает спокойный режим — переопределение стоит ПОСЛЕ общего правила
 * тишины и важнее его (п. 10 директивы). Системной «меньше движения» движение
 * по-прежнему не переживает: там просят тишины целиком. */
@keyframes stkpop {
  0% { transform: scale(1); }
  35% { transform: scale(1.18) rotate(4deg); }
  60% { transform: scale(.94) rotate(-2deg); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: no-preference) {
  .slot--fresh .stk { animation: stkpop 620ms var(--ease-spring); }
}
html[data-calm] .slot--fresh .stk { animation-duration: 620ms !important; animation-iteration-count: 1 !important; }

.stickers--kid { margin-top: var(--s3); }

/* --- альбом в родительском кабинете: те же листы, иначе плотность --------- */
.setblock { margin-top: var(--s4); }
.setblock__name {
  display: flex; align-items: baseline; gap: var(--s2);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-small);
}
.setblock__note { margin-top: var(--s2); font-size: var(--fs-caption); color: var(--ink-muted); }
.setblock .sticker { padding: var(--s2); }

.kidfoot {
  display: flex; justify-content: space-between; align-items: center;
  margin-top: var(--s6); padding-top: var(--s4);
  border-top: 2px solid var(--line);
}
.kidfoot__link { color: var(--ink-muted); font-size: var(--fs-caption); min-height: 44px; display: inline-flex; align-items: center; }
.footlink { display: block; text-align: center; margin-top: var(--s5); color: var(--ink-muted); min-height: 44px; }

/* --------------------------- 19. reward + level-up ----------------------- */

.reward[hidden], .levelup[hidden] { display: none; }

.reward {
  position: fixed; inset: 0; z-index: var(--z-modal);
  display: grid;
  /* Прокрутка вместо обрезки: в landscape телефон карточка выше экрана, а
   * place-items: center в таком случае отрезает ей верх без возможности
   * доскроллить. margin: auto у карточки центрирует, пока есть место. */
  overflow-y: auto;
  /* Когда карточка выше экрана, margin:auto схлопывается в ноль и карточка
   * прилипает к краям — подушка не даёт ей прижаться вплотную. */
  padding: var(--s4);
  background: var(--scrim);
  animation: fade-in var(--dur-base) var(--ease-out);
}
.reward__card {
  margin: auto;
  display: grid; justify-items: center; gap: var(--s2);
  padding: var(--s6); border-radius: var(--r-xl);
  background: var(--surface); box-shadow: var(--sh-3);
  animation: pop var(--dur-pop) var(--ease-spring);
}
.reward__xp {
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--fs-display) * 1.4);
  color: var(--success);
}
/* Вторая валюта — рядом с опытом, но тише: опыт здесь главный, монетки —
 * «и монетки тоже». Отдельная строка (не внутри .reward__xp), чтобы ребёнок
 * видел: дело платит дважды. */
.reward__coins {
  display: inline-flex; align-items: center; gap: .3em;
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--fs-display) * 0.8);
  color: var(--ink);
}
.reward__coins .coin { width: .9em; height: .9em; }
/* `display` выше перебивает скрытие из разметки: без этого правила монетки
 * показывались бы и «+0» — как будто у ребёнка их отняли. */
.reward__coins[hidden] { display: none; }
.reward__sticker {
  display: grid; place-items: center;
  width: 120px; height: 120px; border-radius: var(--r-lg);
  border: 5px solid var(--surface);
  background: var(--amber-t); box-shadow: var(--sh-sticker);
  font-size: calc(var(--fs-display) * 2);
}
/* ALBUM1: в занавесе рисуется ТА ЖЕ наклейка, что ляжет в альбом, — тем же
 * символом из спрайта. Эмодзи остаётся запасом для ключей без рисунка. */
.reward__sticker .stk { width: 92px; height: 92px; }
.reward__sticker .stk-emoji { background: none; box-shadow: none; border-radius: 0; }
.reward__name { font-size: var(--fs-body); font-weight: 700; }

/* M4: подарок за накопленные дела. Строка ниже наклейки и тише её: подарок —
 * событие этого дня, наклейка — событие каждого дела. `[hidden]` — по той же
 * причине, что и у монеток: display выше перебивает скрытие из разметки. */
.reward__gift {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s4);
  border-radius: var(--r-pill);
  background: var(--amber-t);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-title);
}
.reward__gift-emoji { font-size: calc(var(--fs-display) * 1.1); }
.reward__gift-name { font-size: var(--fs-body); }
.reward__gift[hidden] { display: none; }

/* Тот же прокручиваемый оверлей, что .reward и .gameend: с героем карточка
 * подросла, и в landscape телефона place-items:center отрезал бы ей верх. */
.levelup {
  position: fixed; inset: 0; z-index: var(--z-levelup);
  display: grid; overflow-y: auto;
  background: rgba(43, 33, 24, .35);
}
.levelup__card {
  position: relative; z-index: 2;
  margin: auto;
  display: grid; justify-items: center; gap: var(--s1);
  padding: var(--s6) var(--s7); border-radius: var(--r-xl);
  background: var(--surface); box-shadow: var(--sh-3);
  animation: badge-in 600ms var(--ease-spring);
}
.levelup__badge { font-size: calc(var(--fs-display) * 2.4); line-height: 1; }
.levelup__title { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title); }
.levelup__level {
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--fs-display) * 1.6); line-height: 1;
  color: var(--success);
}
.levelup__ring {
  position: absolute; width: 180px; height: 180px;
  border: 4px solid var(--amber); border-radius: var(--r-pill);
  animation: ring-out 900ms var(--ease-out) forwards;
}
.levelup__ring--2 { border-color: var(--coral); animation-delay: 120ms; }

.confetti { position: fixed; inset: 0; z-index: var(--z-confetti); pointer-events: none; overflow: hidden; }
.confetti i {
  position: absolute; top: 0; left: 0;
  width: 10px; height: 14px; border-radius: 3px;
  animation: confetti 700ms var(--ease-out) forwards;
}

/* --------------------------------- 20. toast ----------------------------- */

.toast {
  position: fixed; left: var(--s4); right: var(--s4); bottom: calc(var(--s4) + env(safe-area-inset-bottom));
  z-index: var(--z-toast);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  min-height: 48px; padding: var(--s2) var(--s4);
  border-radius: var(--r-pill);
  background: var(--ink); color: var(--paper);
  box-shadow: var(--sh-3);
  animation: toast-in var(--dur-base) var(--ease-out);
}
.toast__text { font-size: var(--fs-body); line-height: 1.3; }
.toast__undo {
  flex: 0 0 auto; min-height: 36px; padding: 0 var(--s3);
  border-radius: var(--r-pill);
  background: var(--amber); color: var(--ink);
  font-weight: 800; font-size: var(--fs-caption);
}

/* -------------------------------- 21. empty ------------------------------ */

.empty {
  display: grid; justify-items: center; gap: var(--s2);
  padding: var(--s7) var(--s4);
  max-width: 420px; margin: 0 auto; text-align: center;
}
.empty__emoji {
  display: grid; place-items: center;
  width: 96px; height: 96px; border-radius: var(--r-pill);
  background: var(--paper-deep); font-size: var(--fs-display);
}
.empty__title { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-body); }
.empty__hint { color: var(--ink-muted); font-size: var(--fs-caption); }
.empty--page { padding-top: var(--s8); }
.empty--page .btn { margin-top: var(--s3); }

/* -------------------------------- 22. motion ----------------------------- */

@keyframes sticker-in {
  from { opacity: 0; transform: translateY(14px) rotate(0deg) scale(.96); }
}
@keyframes pop {
  0% { transform: scale(1); }
  45% { transform: scale(1.08); }
  100% { transform: scale(1); }
}
@keyframes badge-in {
  0% { transform: scale(.6); opacity: 0; }
  60% { transform: scale(1.06); opacity: 1; }
  100% { transform: scale(1); }
}
@keyframes ring-out {
  from { transform: scale(.6); opacity: .9; }
  to { transform: scale(1.5); opacity: 0; }
}
@keyframes confetti {
  to { transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity: 0; }
}
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-6px); }
  75% { transform: translateX(6px); }
}
@keyframes fade-in { from { opacity: 0; } }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(16px); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  .confetti, .levelup__ring { display: none; }
  /* Подсказка героя — не украшение, а адрес ответа. Пульсирующее кольцо здесь
   * живёт 0.01ms и исчезает, поэтому оставляем статичную обводку. */
  .choice.is-invited { box-shadow: var(--sh-sticker), 0 0 0 6px var(--amber-p); }
}

/* M6: спокойные анимации. То же самое, но включённое человеком: часть детей не
 * может поменять системную настройку, а тумблер — одна кнопка на спокойном
 * экране. Атрибут на <html> ставит public/js/calm.js на каждой детской
 * странице, поэтому правило живёт рядом с системным и не спорит с ним: кто бы
 * ни попросил тишину, результат один. */
html[data-calm] *,
html[data-calm] *::before,
html[data-calm] *::after {
  animation-duration: .01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: .01ms !important;
  scroll-behavior: auto !important;
}
html[data-calm] .confetti,
html[data-calm] .levelup__ring { display: none; }
html[data-calm] .choice.is-invited { box-shadow: var(--sh-sticker), 0 0 0 6px var(--amber-p); }

/* ------------------------------- 23. responsive -------------------------- */

@media (min-width: 640px) {
  .shell { max-width: 760px; }
  .herogrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .form__row--3 { grid-template-columns: repeat(3, 1fr); }
  .form__row--3 .input:first-child { grid-column: auto; }
  .statgrid { gap: var(--s3); }
  /* Шаг 2 раскладки ПК: с 640px дело перестаёт уводить действия на свою
   * строку. На телефоне полная ширина — предсказуемость для пальца (см.
   * комментарий у .taskrow__actions и .pending__actions), на планшете и
   * десктопе та же ширина превратила «Зачёл ✅» в кнопку-ленту через весь
   * ряд. Кнопки сжимаются до подписи, группа уходит вправо, название
   * забирает остальное. У .pending это же правило снимает противоречие
   * с flex-wrap: nowrap на ≥900: строка требовала 100% ширины и не могла
   * ни перенестись, ни ужаться. */
  .taskrow__actions,
  .pending__actions { flex: 0 0 auto; margin-left: auto; }
  .taskrow__actions form,
  .pending__actions form { flex: 0 0 auto; }
  .taskrow__actions .btn,
  .pending__actions .btn { width: auto; }
}

@media (min-width: 900px) {
  .shell { max-width: 880px; }
  .pending { flex-wrap: nowrap; }
}

/* ==========================================================================
   24. Cycle 2 — семьи, код и игры героя.
   Всё в тех же токенах: новый hex в компонентах — баг. Единственное
   исключение ниже — палитра самой игры «Волшебные краски»: там цвета
   являются содержанием занятия, а не оформлением.
   ========================================================================== */

/* --------------------------- 24.1 разделение ролей ----------------------- */

.role__actions { display: flex; flex-wrap: wrap; gap: var(--s2); width: 100%; }

.input--code {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--fs-title);
  letter-spacing: .12em;
  text-align: center;
  text-transform: uppercase;
  min-height: 60px;
}

/* ------------------------------ 24.2 pairing ----------------------------- */

.pairing { display: grid; gap: var(--s3); padding: var(--s4); }
.pairing__hint { font-size: var(--fs-caption); color: var(--ink-muted); }
.pairing__hint--quiet { color: var(--ink-faint); }
.pairing__link { font-size: var(--fs-body); }
/* D5-2: код героя в списке — полноценная тап-цель, а не строка текста:
 * строка с эмодзи и именем слева, код — таблеткой справа, высотой в палец. */
.pairing__hero { display: flex; align-items: center; gap: var(--s2); }
.pairing__hero .link {
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 0 var(--s2); margin-left: auto;
  font-family: var(--font-display); font-weight: 700; letter-spacing: .06em;
}
.pairing__code {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--fs-title) * 1.5);
  letter-spacing: .16em;
  text-align: center;
  padding: var(--s3);
  border-radius: var(--r-md);
  background: var(--amber-t);
  box-shadow: inset 0 0 0 2px var(--amber-d);
}
.divider--inset { margin: 0; }

/* SEC1: смена ПИНа. Редкое действие живёт за своей строкой — то же правило
 * тишины, что и у остальных панелей настроек. */
.pinchange { display: grid; gap: var(--s3); }
.pinchange__summary {
  font-size: var(--fs-label); color: var(--ink-muted);
  cursor: pointer; justify-self: start;
}
.pinchange__summary:hover { color: var(--ink); }
.pinchange[open] .pinchange__summary { color: var(--ink); }

/* --------------------------- 24.3 игры: ссылка --------------------------- */

.gamecta {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s5);
  padding: var(--s4);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 4px solid var(--sky);
  box-shadow: var(--sh-sticker);
  text-decoration: none;
  transform: rotate(-.6deg);
  transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out);
}
.gamecta:hover { transform: rotate(0) translateY(-3px); box-shadow: var(--sh-3); }
/* Вход в комнату героя — про деньги, а не про игры: та же карточка, другой
 * цвет, чтобы новый раздел читался как новый, а не как вторая кнопка игр. */
.gamecta--amber { border-color: var(--amber); }
.gamecta--amber .gamecta__go { background: var(--amber-t); }
.gamecta__emoji { font-size: calc(var(--fs-display) * 1.4); line-height: 1; }
.gamecta__body { flex: 1 1 auto; display: grid; gap: 2px; }
.gamecta__title { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title); }
.gamecta__hint { font-size: var(--fs-caption); color: var(--ink-muted); }
.gamecta__go {
  display: grid; place-items: center;
  width: 44px; height: 44px; border-radius: var(--r-pill);
  background: var(--sky-t); color: var(--ink);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title);
}
/* Вход в выбор вида (R6): обёртка с двумя кнопками, а не одна ссылка, как у
 * соседних дорожек — тап по картинке ведёт к выбору, тап по динамику говорит.
 * В слоте не лапка, а сам герой ребёнка в том, что на него куплено: карточку
 * обязан узнавать тот, кто не умеет читать, ради кого этот раздел и существует.
 *
 * Динамик — тот же угловой .say, что на карточках заданий, а не четвёртая
 * колонка в ряду: в ряду герой 56px + слово «Выбрать» при 28px уже съедают
 * все 232px узкого экрана, и ряд выталкивает страницу вбок — это было
 * поймано на 320px и починено выносом кнопки из потока. */
.gamecta--species { position: relative; }
.gamecta--species .gamecta__link {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: var(--s3);
}
.gamecta--species .gamecta__figure { flex: none; line-height: 0; }
.gamecta--species .gamecta__figure .hero { --hero-size: 56px; }
.gamecta--species .gamecta__body { min-width: 0; }
/* Динамик висит на углу (top/right −8px, ширина 56px) и занимает правый
 * верх карточки. Стрелка — последний flex-элемент в потоке: без резерва она
 * подползает ровно под динамик, и на 320px круги соприкасаются (0.6px по
 * вертикали, а тени кругов ложатся друг на друга). Резерв уводит стрелку
 * влево от неё. Заголовок при этом не при чём: он жмётся по тексту и сам
 * не достаёт до кнопки. */
.gamecta--species .gamecta__link { padding-right: 36px; }

/* R7: приглашение назвать героя. Тот же язык карточек, что у входов в комнату
 * и в игры, но с двумя отличиями по смыслу: рамка пунктиром — это незаконченное
 * дело, а не «куда пойти»; и вся карточка — кнопка, потому что трёхлетний не
 * попадёт в кружок 44px. Показывается, только пока имени нет: назвали — и
 * карточка исчезает вместе с собой. */
.namecta {
  position: relative;
  display: flex; align-items: center; gap: var(--s2);
  width: 100%; margin-top: var(--s5); padding: var(--s4);
  border: 4px dashed var(--coral); border-radius: var(--r-lg);
  background: var(--surface); box-shadow: var(--sh-sticker);
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  transform: rotate(-.6deg);
  transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.namecta:hover { transform: rotate(0) translateY(-3px); box-shadow: var(--sh-3); }
.namecta:active { transform: scale(.98); }
/* Ярлык меньше, чем у входов в комнату и игры: на 320px он делит строку с
 * текстом, и крупный съедает место у заголовка. */
.namecta__emoji { font-size: var(--fs-display); line-height: 1; }
.namecta__body { flex: 1 1 auto; display: grid; gap: 2px; min-width: 0; padding-right: 48px; }
.namecta__title { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title); }
.namecta__hint { font-size: var(--fs-caption); color: var(--ink-muted); }
/* Значок звука стоит в углу, как на карточках заданий, а не в строке: угловое
 * место не отнимает ширину у текста, и карточка читается как одна кнопка. */
.namecta__go {
  position: absolute; top: var(--s3); right: var(--s3);
  display: grid; place-items: center;
  width: 44px; height: 44px; border-radius: var(--r-pill);
  background: var(--coral-t); color: var(--ink); font-size: 20px;
}
/* Кнопка говорит — рамка зеленеет: тот же ответ, что у динамиков на заданиях. */
.namecta.is-speaking { border-color: var(--green); border-style: solid; }
.namecta.is-speaking .namecta__go { background: var(--green-t); }

/* M2: событие дня. Тот же язык карточек, что у приглашения назвать героя, но
 * рамка сплошная, а не пунктиром: пунктир в этом языке значит «незаконченное
 * дело», а новость дня закончена с самого утра. Голубой — цвет подсказок, а не
 * награды: событие не претендует на место «Готово!» и не спорит с ним за
 * внимание. Значок звука — колонкой, а не в углу: строка новости короткая, и
 * угловое место оставило бы под текстом пустой правый край. */
.kidevent {
  position: relative;
  display: flex; align-items: center; gap: var(--s2);
  width: 100%; margin-top: var(--s5); padding: var(--s3) var(--s4);
  border: 3px solid var(--sky-d); border-radius: var(--r-lg);
  background: var(--sky-t);
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.kidevent:active { transform: scale(.98); }
/* Эмодзи — заголовочный, а не дисплейный: в детском режиме --fs-display — 40px,
 * и на 358px он вместе с кнопкой звука оставлял заголовку меньше 170px —
 * «Двойной опыт» переносился, и лента становилась карточкой в два этажа. */
.kidevent__emoji { flex: none; font-size: var(--fs-title); line-height: 1; }
/* Заголовок — и есть новость на экране: одна строка, крупно, как у карточки
 * «Назови героя». Полная фраза — голос, ей тут места нет. */
.kidevent__title {
  flex: 1 1 auto; min-width: 0;
  /* Размер текста, а не заголовка: в детском режиме --fs-title — 28px, и
   * «Двойной опыт» на 358px ломался в две строки. Новости хватает и 22px. */
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
}
.kidevent__go {
  flex: none; display: grid; place-items: center;
  width: 44px; height: 44px; border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink);
}
/* Новость дышит, пока её не услышали, и засыпает после первого тапа: ребёнок
 * видит, что здесь есть что послушать, но лента не мигает весь день. Дышит
 * только прозрачность — геометрия двигала бы и строку рядом с ней. */
.kidevent.is-new .kidevent__go { animation: kidevent-breathe 2.4s var(--ease-out) infinite; }
.kidevent.is-speaking { border-color: var(--green); }
.kidevent.is-speaking .kidevent__go {
  background: var(--green-t); animation: none;
}
@keyframes kidevent-breathe { 0%, 100% { opacity: .4; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .kidevent.is-new .kidevent__go { animation: none; }
}

/* HABIT: тот же каркас ленты, зелёный — как зачтённое дело. Цвет вместо
 * отдельной разметки: ребёнок уже знает зелёный как «получилось», и новость
 * «ты теперь делаешь это сам» читается тем же словом. Геометрию не меняем —
 * заголовок в одну строку на 320px уже выверен у новости дня. */
.kidevent--habit { border-color: var(--green-d); background: var(--green-t); }
.kidevent--habit.is-speaking { border-color: var(--green); }

/* --------------------------- 24.4 игры: список --------------------------- */

.shell--games { padding-bottom: var(--s7); }
.gameshub__head {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s4);
}
/* min-width: 0 — сетка по умолчанию не сжимается ниже длинного слова, и
 * «Комната героя» выталкивала тумблер звука за край экрана (360px). */
.gameshub__titlebox { display: grid; gap: 2px; min-width: 0; }
/* Размер заголовка комнаты, а не домашнего экрана: на 390px «Игры героя»
 * дисплеем переносилось на две строки и съедало первый ряд плиток. Ребёнок
 * узнаёт экран по голосу и по плиткам, а не по размеру заголовка. */
.gameshub__title { font-size: var(--fs-title); line-height: 1.15; }
.gameshub__sub { color: var(--ink-muted); font-size: var(--fs-caption); }

/* GAMES2: кап виден ДО игры — три монетки в шапке каталога, гаснущие по
   мере оплаченных партий дня, завтра снова полные. Без цифр: нечитающий
   ребёнок считает их глазами, а тап объясняет голосом (первый визит —
   сам). Ряд тихий и один на весь экран: это новость шапки, а не плиток. */
.gamehub__pays {
  justify-self: start;
  display: inline-flex; align-items: center; gap: 3px;
  margin-top: 2px;
  /* Пол тапа: пилюля тапается (объясняет кап голосом) и была 19px высотой —
   * палец ребёнка в неё не попадал. */
  min-height: 44px; padding: 2px 12px; border-radius: var(--r-pill);
  background: var(--amber-t);
  transition: transform var(--dur-fast) var(--ease-spring);
}
.gamehub__pays:active { transform: scale(.95); }
.gamehub__pays .coin { width: 13px; height: 13px; }
.gamehub__pays .is-off { opacity: .3; }

.gamegrid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s3); margin-top: var(--s5);
}

/* GAMES1: каталог по навыкам. Заголовок секции — подпись label-каскада:
   заглавные, лёгкий трекинг, приглушённые чернила. Эмодзи навыка при ней
   маленький сознательно: навык читает родитель, ребёнок выбирает не раздел,
   а плитку, и крупный эмодзи здесь спорил бы с плитками за внимание (D2).
   Секция не липнет к краю: категорий шесть, в каждой пока 1–3 плитки, и
   липнущий заголовок среди двух карточек — шум, а не навигация; приклеим,
   когда секция перестанет влезать в экран. */
.gameshub__cat { margin-top: var(--s5); }
.gameshub__cat-title {
  display: flex; align-items: center; gap: var(--s2);
  margin: 0 0 var(--s2);
  font-family: var(--font-body); font-size: var(--fs-caption);
  font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-muted);
}
/* Цикл 46, «✨ Новенькое»: та же полка, что у категорий, — но заголовок
   теплее базового серого (приглашение, а не структура каталога) и первый
   выезд полки мягкий. Спокойный режим гасит и его: calm не должен
   встречать ребёнка анимацией (M6 — гасим трансформ у заголовка, у
   плиток уже гасится). */
.gameshub__cat--fresh { margin-top: var(--s4); }
.gameshub__cat-title--fresh { color: var(--berry); }
@media (prefers-reduced-motion: no-preference) {
  .gameshub__cat--fresh .gamecard:not(.gamecard--locked) { animation: fresh-in .38s ease-out backwards; }
  .gameshub__cat--fresh .gamecard:nth-child(2) { animation-delay: .08s; }
  .gameshub__cat--fresh .gamecard:nth-child(3) { animation-delay: .16s; }
}
@keyframes fresh-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
.gameshub__cat-title span[aria-hidden] { font-size: 20px; line-height: 1; }
.gamegrid--cat { margin-top: 0; }
.gamecard {
  position: relative;
  display: grid; gap: var(--s1); justify-items: center; text-align: center;
  padding: var(--s4) var(--s3);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 4px solid var(--surface);
  box-shadow: var(--sh-sticker);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out);
}
.gamegrid > .gamecard:nth-child(odd) { transform: rotate(-1.1deg); }
.gamegrid > .gamecard:nth-child(even) { transform: rotate(1deg); }
a.gamecard:hover { transform: rotate(0) translateY(-4px); box-shadow: var(--sh-3); }
.gamecard__emoji { font-size: calc(var(--fs-display) * 1.5); line-height: 1; }
.gamecard__name { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body); }
/* Цикл 50: слово названия шире колонки («изменилось?» в Baloo 2 — 144px
 * против ~100px бокса) завернуть нельзя, переносов нет — плитка вылезала
 * за страницу на 320. Такая плитка получает весь ряд: название остаётся
 * крупным, а не усаживается до нечитаемого. Класс ставит hub.js ПОСЛЕ
 * загрузки шрифта, измерением, а не подсчётом букв — метрики шрифта
 * серверу неизвестны. */
.gamecard--span2 { grid-column: 1 / -1; }
.gamecard__hint { font-size: var(--fs-caption); color: var(--ink-muted); }
.gamecard__play {
  margin-top: var(--s2);
  padding: 2px 14px; border-radius: var(--r-pill);
  background: var(--green-t); color: var(--green-p);
  font-weight: 800; font-size: var(--fs-caption);
}
.gamecard--locked {
  background: var(--paper-deep); border-color: var(--paper-deep);
  box-shadow: none;
}
.gamecard--locked .gamecard__emoji { opacity: .45; filter: grayscale(.55); }
.gamecard--locked .gamecard__name { opacity: .55; }
.gamecard--locked .gamecard__hint { color: var(--ink-faint); }
.gamecard__lock {
  margin-top: var(--s2);
  padding: 2px 14px; border-radius: var(--r-pill);
  background: var(--surface);
  box-shadow: inset 0 0 0 2px var(--line);
  font-weight: 800; font-size: var(--fs-caption); color: var(--ink-muted);
}
.gameshub__tease {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s5); padding: var(--s2) var(--s4);
  border-radius: var(--r-md); background: var(--amber-t);
  font-size: var(--fs-body); text-align: left;
}
.gameshub__tease .hero { --hero-size: 64px; flex: none; }
.gameshub__tease-text { min-width: 0; }

/* --------------------------- 24.5 игры: экран ---------------------------- */

.gamehead { display: flex; align-items: center; gap: var(--s3); margin-top: var(--s4); }
/* Цикл 50: в кадре 320 ряд «назад • название • голос» не помещался —
 * min-width:auto гнал кнопку голоса за край страницы, а сама кнопка
 * усаживалась 44→36, ниже пола тапа. Название ужинается и заворачивается
 * (слово длиннее строки — см. overflow-wrap ниже), кнопка голоса не
 * усаживается вовсе: тап-таргет не участвует в спорах за ширину. */
.gamehead__main { display: grid; gap: 2px; min-width: 0; }
.gamehead__title { font-size: var(--fs-title); line-height: 1.2; overflow-wrap: anywhere; }
.gamehead__sub { color: var(--ink-muted); font-size: var(--fs-caption); }
.gamehead .voice-toggle { flex: 0 0 auto; }
/* Кадры уже 360 (SE-класс и эмуляции уже): «изменилось?» — первое слово
 * названия игры, которое в title-кегле (28) шире свободных ~148px ряда.
 * Кегль body (22) оставляет запас, anywhere выше ловит разбег метрик
 * шрифта между движками. */
@media (max-width: 359px) {
  .gamehead__title { font-size: var(--fs-body); }
}
.gamehead__back {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 56px; height: 56px; border-radius: var(--r-pill);
  background: var(--surface); box-shadow: var(--sh-1);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title);
  text-decoration: none;
}
.gamehead__back:active { transform: scale(.95); }

.gameprogress { display: flex; justify-content: center; gap: var(--s2); margin-top: var(--s4); }
.gameprogress i {
  width: 14px; height: 14px; border-radius: var(--r-pill);
  background: rgba(43, 33, 24, .14);
  transition: background-color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-spring);
}
.gameprogress i.is-done { background: var(--green); transform: scale(1.12); }

/* GAMES2: часы time-игр. Тихая подпись под точками прогресса, а не заголовок:
   табличные цифры (тик не дёргает строку), приглушённые чернила, ни пульса,
   ни цвета — информация, а не давление; обратного отсчёта нет по построению.
   Спокойный режим (M6) прячет их целиком: тикающий счётчик — ровно тот
   стимул, который calm существует убирать. Время всё равно мерится молча и
   возвращается итогом на карточке финиша, где тикать уже нечему. */
.gameclock {
  margin-top: var(--s2); text-align: center;
  color: var(--ink-muted); font-size: var(--fs-caption);
  font-variant-numeric: tabular-nums;
}
html[data-calm] .gameclock { display: none; }

.gameprompt {
  margin-top: var(--s4); text-align: center;
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title);
  min-height: 1.4em;
}
.gameprompt[hidden] { display: none; }

.gameboard { margin-top: var(--s4); }
/* Цикл 45: трек gamelead — во всю ширину доски. Прежняя неявная auto-
 * колонка сжималась до max-content, и опорный ряд отличий вставал у́же
 * поля — колонки сличения разъезжались. Счётные линии и кнопки-динамики
 * остальных игр центрены justify-items'ом и полной ширины не заметят. */
.gamelead { display: grid; grid-template-columns: minmax(0, 1fr); justify-items: center; margin-bottom: var(--s4); }
.gamecountline { font-size: calc(var(--fs-display) * 1.5); line-height: 1.2; text-align: center; letter-spacing: .08em; }

/* GAMES-ENGINE (цикл 57): подиум движковой сцены. Высота ФИКСИРОВАННАЯ —
   это условие DOM-контракта (ратификация CTO): гейты кадра меряют низ
   [data-board], и растягивать подиум по контенту нельзя, иначе «раунд на
   фолде» мерял бы пустоту. Канвас сидит внутри один в один, ничего наружу
   не вылезает (overflow — ограда, а не скроллер: вылезшая сцена — дефект
   вёрстки, а не жест). Ширина во всю доску — иначе justify-items:center
   у .gamelead сжал бы подиум до max-content (урок лесенки, цикл 45).
   Вьюпортные единицы, не контейнерные: cqi на самом элементе мерял бы
   не тот ящик (контейнер не меряет сам себя). */
.gamestage {
  position: relative;
  width: 100%;
  height: clamp(148px, 46vw, 224px);
  overflow: hidden;
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--sh-sticker);
}
.gamestage__canvas { display: block; width: 100%; height: 100%; }

.choices { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s3); }

/* GAMES1: «Найди всех» — поле из 6–8 карточек. Цели остаются большими (палец
   ребёнка), но этажом ниже: три ряда по 116px вместе с подсказкой на экран
   телефона не влезают, а листать поле в поисках цели — уже не
   внимательность, а прокрутка. */
.choices--field { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.choices--field .choice { min-height: 84px; padding: var(--s2); }

/* Цикл 45, «Лесенка чисел»: ряд числовых плашек с дыркой. Плашка — тот же
   стикер-язык, что кнопки-ответы (bignum-шрифт: цифры рисует шрифт, не
   платформенный кейкап); дырка — пунктирная рамка «здесь было» (прецедент
   memhole), знак вопроса внутри — вопрос игры, ребёнку читать не нужно.
   Правка №1 критика-45: расписание растёт до шести ступеней, и плашка
   масштабируется их числом — клиент ставит --steps, контейнер-запрос даёт
   кеглю ехать за фактической шириной ряда (flex с nowrap сам не сжимается:
   6 ступеней = 412px против 272px контента на 320). width:100% обязателен:
   justify-items:center на gamelead иначе сжал бы ряд до max-content и
   контейнер мерил бы самого себя. Двузначное «10» обязано влезать в плашку
   (кегль 0.7×ширины, боковой отступ нулевой: контент = плашка − рамки). */
.ladderrow {
  display: flex; justify-content: center; gap: var(--s2); flex-wrap: nowrap;
  width: 100%; container-type: inline-size;
  font-family: var(--font-display); font-weight: 800;
  line-height: 1;
}
.ladderstep {
  min-width: min(calc(var(--fs-display) * 1.55),
    calc((100cqi - (var(--steps, 4) - 1) * var(--s2)) / var(--steps, 4)));
  font-size: min(calc(var(--fs-display) * 1.1),
    calc((100cqi - (var(--steps, 4) - 1) * var(--s2)) / var(--steps, 4) * 0.7));
  padding: var(--s2) 0;
  border-radius: var(--r-md); text-align: center;
  background: var(--surface); border: 3px solid var(--surface);
  box-shadow: var(--sh-sticker);
}
.ladderstep--hole {
  border: 3px dashed var(--ink-faint); background: transparent;
  box-shadow: none; color: var(--ink-faint);
}

/* Цикл 45, «Найди отличия»: опорный ряд и поле — одна и та же сетка
   (колонки совпадают один в один, сличение честное); --cols едёт на обоих
   контейнерах из клиента. Пять колонок — потолок: на 320px ячейка остаётся
   крупной для пальца, а эмодли в рядах мельче полей «найди всех» — их
   сличают, а не тапают наугад. Гэпы ряда и поля ходят ПАРОЙ (расхождение
   сдвигает колонки и ломает сличение), но на узком кадре пара НЕ следует
   за .choices: пять колонок с гэпом s4 дают клетку 41.6px — ниже пола
   тапа 44px (замер критика-45, правка №2). justify-items:center — глифы
   ряда стоят по центру ячейки, как содержимое кнопок поля (правка №5):
   прижатые влево, они смещали пару «было→стало» до 10px. */
.diffrow {
  display: grid; width: 100%; justify-items: center;
  grid-template-columns: repeat(var(--cols, 5), minmax(0, 1fr)); gap: var(--s3);
}
.diffrow .bigemoji { font-size: calc(var(--fs-display) * 1.05); }
.choices--diffrow {
  grid-template-columns: repeat(var(--cols, 5), minmax(0, 1fr));
}
.choices--diffrow .choice { min-height: 68px; padding: var(--s1); }
.choices--diffrow .bigemoji { font-size: calc(var(--fs-display) * 1.05); }

/* «Больше или меньше»: тарелка из зверят внутри кнопки-ответа. Слияние двух
   независимых лекарств одной болезни (premerge cycle62): цикл 56 расширил
   саму кнопку — ответы ДВЕ колонки (.choices--moreless), тарелка живёт в
   половине экрана, а не в трети; верификатор integ добавил потолок кегля —
   контейнер .morefit (урок: контейнер НЕ мерит сам себя, прецедент
   .ladder-row), колонки minmax(0,1fr) не дают авто-колонкам отказаться
   сжиматься, clamp держит кегль внутри фактической ширины кнопки. Оба
   оставлены: кнопка шире, кегль гарантированно внутри — ни одно из
   измеренных обеими ветками переполнений (+22px@375, +96px@320 у 56-го;
   +22px@375, +41px@320 у верификатора) не воспроизводится. */
.morefit { display: block; width: 100%; container-type: inline-size; }
.moreplate {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s1);
  justify-items: center;
  font-size: clamp(12px, 24cqi, calc(var(--fs-display) * 1.1)); line-height: 1.1;
}
.choices--moreless { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* «Что пропало?»: оставшиеся предметы и дырка на месте пропавшего. Дырка —
   пунктирная рамка: «здесь было», а не новая кнопка. */
.memitem, .memhole { display: inline-block; font-size: calc(var(--fs-display) * 1.2); line-height: 1.15; }
.memhole {
  min-width: 1.4em; padding: 0 .1em;
  border: 4px dashed var(--ink-faint);
  border-radius: var(--r-md);
  color: var(--ink-faint); font-family: var(--font-display); font-weight: 800;
  text-align: center;
}
.choice {
  display: grid; place-items: center;
  min-height: 116px; padding: var(--s3);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 4px solid var(--surface);
  box-shadow: var(--sh-sticker);
  /* Тот же быстрый детский тап: без этого браузер может отдать второй тап
   * под зум, и клик по ответу не случится вовсе. */
  touch-action: manipulation;
  animation: sticker-in var(--dur-slow) var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 45ms);
  transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out);
}
.choice:nth-child(odd) { transform: rotate(-1.2deg); }
.choice:nth-child(even) { transform: rotate(1.1deg); }
.choice:active { transform: scale(.97); }
.choice--right { border-color: var(--green); box-shadow: var(--sh-3); animation: pop var(--dur-pop) var(--ease-spring); }
.choice--wrong { opacity: .4; animation: shake 380ms var(--ease-out); }
.choice:disabled { cursor: default; }

.swatch {
  display: block; width: 72px; height: 72px; border-radius: var(--r-md);
  background: var(--sw, var(--paper-deep));
  box-shadow: inset 0 0 0 3px rgba(43, 33, 24, .08), var(--sh-1);
}
.bignum, .bigletter {
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--fs-display) * 1.6); line-height: 1;
}
.bigemoji { font-size: calc(var(--fs-display) * 1.4); line-height: 1; }
.shape { width: 80px; height: 80px; fill: var(--shape, var(--sky-p)); }

/* Цикл 50, «Что изменилось?»: сцена запоминания — четыре круга в ряд
   (56px: четыре круга с гэпом s3 влезают в 320 — 4×56+3×12=260; мельче
   нельзя — перемену должен видеть глаз). Выборы — та же четвёрка той же
   сеткой и в ТОМ ЖЕ порядке: место перемены и есть ответ, кнопки не
   мешаются. Миг — короткое притемнение поля: «было так, стало так»;
   спокойный режим сжимает его в миг — игра остаётся игрой: цвет-то
   сменился. */
.changescene { display: flex; justify-content: center; gap: var(--s3); }
.changescene__shape { width: 56px; height: 56px; }
.choices--scene { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.choices--scene .choice { min-height: 84px; padding: var(--s2); }
.choices--scene .shape { width: 56px; height: 56px; }
.choices--scene.is-blink { animation: blinkdip 600ms var(--ease-out); }
@keyframes blinkdip {
  30% { filter: brightness(.45); }
  100% { filter: brightness(1); }
}

/* Контейнер слова — сам .gameword: он всегда во всю ширину lead-колонки,
 * кегль читаемого слова едет за ней (15cqi): «ЗЕБРА» с трекингом .1em при
 * фиксированных 44.8px вылезала из доски на 320 (+16px, гейт лестницы).
 * Урок «контейнер не мерит сам себя»: width:100% обязателен. */
.gameword { display: grid; justify-items: center; gap: var(--s2); width: 100%; container-type: inline-size; }
.gameword__emoji { font-size: calc(var(--fs-display) * 2.4); line-height: 1; }
.gameword__text {
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--fs-display) * 1.1); letter-spacing: .1em;
  padding: 2px 18px; border-radius: var(--r-pill);
  background: var(--amber-t);
}

/* ------------------- 24.5b игры на чтение: слоги и слова ------------------ */

/* Общее для двух игр: кнопка «послушай слово» и слоговые стикеры. Оба — тот же
 * стикер, что .say на карточках заданий, но в потоке, а не в углу. Состояние
 * is-speaking вешает тот же voice.js; цвет волны берётся из --say-ring, чтобы
 * блок волн не пришлось копировать под каждый компонент. */
.sayword,
.gamsyl__chip {
  position: relative;
  border: 3px solid var(--surface);
  box-shadow: var(--sh-sticker);
  transition: transform var(--dur-fast) var(--ease-spring), background-color var(--dur-fast) var(--ease-out);
}
.sayword.is-speaking,
.gamsyl__chip.is-speaking { color: var(--surface); }
.sayword.is-speaking::before,
.sayword.is-speaking::after,
.gamsyl__chip.is-speaking::before,
.gamsyl__chip.is-speaking::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  border: 3px solid var(--say-ring, var(--green)); opacity: 0;
  animation: say-ring 1.5s var(--ease-out) infinite;
}
.sayword.is-speaking::after,
.gamsyl__chip.is-speaking::after { animation-delay: .75s; }

/* 56px, как .say: слово на экране — задание, кнопка — только помощь. */
.sayword {
  width: 56px; height: 56px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--amber); color: var(--ink);
  font-size: 26px; line-height: 1;
}
.sayword:active { transform: scale(.92); }
.sayword.is-speaking { background: var(--green); }

/* Первый раунд «Прочитай слово»: ребёнок ещё не знает, что кнопку можно
 * нажать, поэтому она сама подмигивает — и молчит, потому что автоозвучка
 * отменила бы задание. Гаснет через 2.4s из words.js. */
.sayword.is-inviting:not(.is-speaking) {
  animation: say-invite 1.1s var(--ease-out) infinite alternate;
}
.sayword.is-inviting:not(.is-speaking)::before,
.sayword.is-inviting:not(.is-speaking)::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  border: 3px solid var(--amber); opacity: 0;
  animation: say-ring 1.1s var(--ease-out) infinite;
}
.sayword.is-inviting:not(.is-speaking)::after { animation-delay: .55s; }
@keyframes say-invite {
  from { transform: scale(1); }
  to { transform: scale(1.09); }
}

/* Слоги. Каждый стикер произносит себя, после верного ответа слоги
 * съезжаются в целевое слово — тот самый момент, когда ребёнок видит чтение.
 * Буквы на стикере — чернила, а не голубой: это единственное место, где
 * ребёнок всерьёз всматривается в буквы, и им положено быть самыми чёткими. */
.gamsyl { display: grid; justify-items: center; gap: var(--s4); }
/* flex-wrap: три стикера по min-width 96 + два гэпа s3 — это 312px против
   309px контента на 320: слово из трёх слогов вылезало на 3px (гейт
   лестницы 53-го, первый прогон; real-but-intermittent — прибор промахивался
   раундом). Перенос последнего слога на свою строку, кегль не тронут
   (пре-регистрация §2.2: перенос, не мельчение). */
.gamsyl__row {
  display: flex; flex-wrap: wrap; justify-content: center;
  align-items: center; gap: var(--s3);
}
.gamsyl__chip {
  --say-ring: var(--sky);
  min-width: 96px; min-height: 84px; padding: 0 var(--s4);
  border-radius: var(--r-md);
  background: var(--sky-t); color: var(--ink);
  font-family: var(--font-display); font-weight: 800; line-height: 1;
  font-size: calc(var(--fs-display) * 1.1); letter-spacing: .04em;
}
.gamsyl__chip:active { transform: scale(.94); }
.gamsyl__chip.is-speaking { background: var(--sky-p); }
/* Узкие телефоны: два слога по 3 буквы («ЛАМ·ПА» 280px, «БА·НАН» 274px при
 * 272 доступных) переносили чипс-ряд на вторую строку (+96px) и роняли доску
 * за фолд на 320×481 — низ 503, три кнопки не видны (пин-зонд 09-07, 2 из 12
 * слов пула). Паддинг s4→s3 в этой полосе: ряд 265px, низ 407, запас 7px,
 * кегль не тронут (пре-регистрация §2.2: перенос, не мельчение). */
@media (max-width: 359px) {
  .gamsyl__chip { padding: 0 var(--s3); }
}
.gamsyl__dash { color: var(--ink-muted); font-size: var(--fs-display); line-height: 1; }
.gamsyl__whole {
  font-family: var(--font-display); font-weight: 800; line-height: 1;
  font-size: calc(var(--fs-display) * 1.4); letter-spacing: .08em;
  padding: var(--s3) var(--s5); border-radius: var(--r-pill);
  background: var(--green-t); color: var(--green-p);
  animation: pop var(--dur-pop) var(--ease-spring);
}

/* Слово, которое ребёнок читает сам: крупнее, чем в «Первой букве», — здесь
 * оно и есть задание, а не подсказка. */
.gameword--read { gap: var(--s3); }
.gameword--read .gameword__text {
  font-size: clamp(20px, 15cqi, calc(var(--fs-display) * 1.6));
  background: var(--green-t); color: var(--green-p);
}

/* --------------- 24.5c цикл 43: забор «Кто спрятался?» и корзины -------- */

/* Забор статичен по построению (§5, вердикт критика): ни одного кадра
 * анимации — доски нарисованы слоями градиентов и сами ничего не делают.
 * Зверь — один большой эмодзи в окне с overflow: hidden; доски закрывают
 * нижние 44% окна, и верх виден всем десяти зверям пула — проверено
 * пиксельным пробором: у самого «низкого» глифа пула (🦔 иголки
 * начинаются на 42-й строке из 112) над досками остаётся 21 строка
 * иголок, у остальных 35–63. Вскрытие — ОДНА смена состояния: класс
 * is-open уводит доски вниз одним переходом, без цикла и стробинга; calm
 * и prefers-reduced-motion гасят переход в миг, и забор просто исчезает. */
.peekstage { display: flex; align-items: flex-end; justify-content: center; gap: var(--s3); }
/* Клип ниже — ЗАМЫСЕЛ, не дефект (гейт probe-frame-ladder.mjs печатает его
 * INFO по правилу «обрезан предком»): overflow: hidden прячет зверя за
 * забором — глиф кегля 264px рендерится в 329px против окна 150px, вылет
 * +179px, и доски, налегающие на 6px за обе кромки окна (+12px). Режущий
 * предок лежит внутри раунда, за клипом ни кнопки, ни слова — только
 * декоративный глиф. Убирать overflow нельзя: зверь перестанет прятаться. */
.peekfence {
  position: relative;
  width: 150px; height: 112px;
  overflow: hidden;
  border-radius: var(--r-md);
  background: linear-gradient(180deg, var(--sky-d), var(--sky-t));
  box-shadow: var(--sh-sticker), inset 0 0 0 3px var(--surface);
}
.peekfence__animal {
  position: absolute; top: -6px; left: 50%;
  transform: translateX(-50%);
  font-size: 264px; line-height: 1;
}
.peekfence__planks {
  position: absolute; left: -6px; right: -6px; bottom: 0;
  height: 44%;
  background:
    repeating-linear-gradient(90deg,
      var(--amber) 0 26px, var(--amber-p) 26px 30px, var(--amber-d) 30px 34px),
    linear-gradient(0deg, rgba(43, 33, 24, .25), rgba(43, 33, 24, .05) 85%);
  border-top: 5px solid var(--amber-p);
  box-shadow: 0 -6px 12px rgba(43, 33, 24, .16);
  transition: transform var(--dur-slow) var(--ease-spring);
}
.peekfence.is-open .peekfence__planks { transform: translateY(120%); }

/* «Разложи по корзинам»: корзину-кнопку нечитающий узнаёт лицом, слово
 * под ним — родителю и aria. Мелкое и приглушённое: не спорит с лицом. */
.basketword {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-caption); letter-spacing: .04em;
  color: var(--ink-muted);
}

/* --------------- 24.5d цикл 44: тень и «Два одинаковых» ------------------ */

/* Силуэт — тот же глиф, что цветной предмет наверху, минус цвет: фильтр
 * висит на внутреннем span и не трогает кнопку — у неё свои состояния
 * (wrong гасит карточку, right зеленит бордер), и brightness(0) на кнопке
 * целиком съел бы их вместе с цветом. Только чернота, без opacity и
 * contrast: карточка под глифом остаётся светлой, и тень читается как
 * «предмет без цвета», а не чёрная дыра. Тёмной детской темы в продукте
 * нет — токены paper/ink светлые на всех детских экранах, — поэтому
 * запасная обводка не нужна; различимость пар силуэтов меряется пиксельным
 * зондом scripts/review-games44.mjs на 358 и 320. Полю «Два одинаковых»
 * сверх .choices--field ничего не нужно: те же крупные карточки-цели, тот
 * же поп на найденной паре; тряска промаха — общая, и она уже уважает
 * спокойный режим: глобальные правила reduce-motion и html[data-calm]
 * гасят длительности в .01ms, а сами кадры shake — только translateX. */
.shadowglyph { filter: brightness(0); }

/* --------------------------- 24.6 игра «Найди пару» --------------------- */

.memgrid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s3);
  perspective: 800px; -webkit-perspective: 800px;
}
.memcard {
  aspect-ratio: 3 / 4; min-height: 96px; padding: 0;
  background: none; border: 0;
  /* Ребёнок стучит по карточкам быстро, и на части браузеров второй быстрый
   * тап браузер забирает под двойное касание-зум: клика не бывает вовсе.
   * touch-action не наследуется, поэтому на body он не спасает карточку. */
  touch-action: manipulation;
  animation: sticker-in var(--dur-slow) var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 40ms);
}
.memcard__inner {
  /* B4, настоящая причина: inner — <span>, а span строчный, и width/height
   * на нём не действуют. Обёртка и обе стороны оставались 0×0, на карточке
   * был виден только выпирающий знак вопроса из ::after, и тап выглядел
   * как «ничего не произошло». display: block делает 100% высоты и ширины
   * настоящими, стороны получают площадь, и переворот наконец что-то
   * показывает. Трёхмерность ниже — уже вторая половина истории. */
  display: block;
  position: relative; width: 100%; height: 100%;
  transform-style: preserve-3d; -webkit-transform-style: preserve-3d;
  transition: transform var(--dur-slow) var(--ease-spring);
}
.memcard.is-open .memcard__inner { transform: rotateY(180deg); }
.memcard__face {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  border-radius: var(--r-md);
  /* -webkit- здесь не украшение. WebKit до 15.4 понимает только префиксную
   * форму и рисует ОБЕ стороны сразу: животное видно на ещё закрытой
   * карточке, а по тапу уходит под знак вопроса. Со стороны это выглядит
   * ровно как «карточки не переворачиваются» — B4 из Update 8. */
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  box-shadow: var(--sh-sticker);
}
.memcard__face--back {
  background: var(--paper-deep);
  box-shadow: var(--sh-sticker), inset 0 0 0 3px var(--amber);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title);
  color: var(--ink-faint);
  /* B4, диагноз владельца: у рубашки обязан быть СВОЙ transform. Без него
   * ряд версий WebKit считает элемент нетрансформированным и не прячет его
   * по backface-visibility — «?» остаётся поверх животного, и выглядит это
   * как «карточки не переворачиваются». rotateY(0) не меняет геометрию, но
   * вводит элемент в трёхмерную сцену, где hidden начинает работать. */
  transform: rotateY(0deg); -webkit-transform: rotateY(0deg);
  /* Страховка на случай, если hidden проигнорирован всё равно: открытая
   * карточка прячет рубашку совсем. Ребёнок тапал ради животного — пусть
   * видит животное. Прозрачность только в открытом состоянии, чтобы в
   * закрытом не уплощать слой и не мешать перевороту. */
  transition: opacity var(--dur-slow) var(--ease-out);
}
.memcard.is-open .memcard__face--back { opacity: 0; }
.memcard__face--back::after { content: "?" / ""; }
.memcard__face--front {
  background: var(--surface);
  transform: rotateY(180deg);
  font-size: calc(var(--fs-display) * 1.3);
}
.memcard.is-matched .memcard__face--front { background: var(--green-t); }
.memcard.is-matched { animation: pop var(--dur-pop) var(--ease-spring); }

/* ----------------------------- 24.7 конец игры --------------------------- */

.gameend {
  position: fixed; inset: 0; z-index: var(--z-modal);
  display: grid; padding: var(--s4);
  /* Та же причина, что у .reward: в landscape карточка выше экрана. */
  overflow-y: auto;
  background: var(--scrim);
  animation: fade-in var(--dur-base) var(--ease-out);
}
.gameend[hidden] { display: none; }
.gameend__card {
  margin: auto;
  display: grid; justify-items: center; gap: var(--s2);
  padding: var(--s6) var(--s5); border-radius: var(--r-xl);
  background: var(--surface); box-shadow: var(--sh-3);
  animation: pop var(--dur-pop) var(--ease-spring);
  max-width: 360px; width: 100%;
}
.gameend__stars { font-size: calc(var(--fs-display) * 1.4); letter-spacing: .06em; }
.gameend__title { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-display); }
.gameend__score { color: var(--ink-muted); font-size: var(--fs-caption); }
/* GAMES2: итог времени — строка, а не секундомер; цифры табличные, чтобы
   «1:23» не прыгало при появлении. Показывается и в спокойном режиме:
   прятался живой чип, а не факт. */
.gameend__time {
  color: var(--ink-muted); font-size: var(--fs-caption);
  font-variant-numeric: tabular-nums;
}
/* GAMES2: выплата. Приезжает в карточку мигом позже праздника и может не
   приехать вовсе — карточка уже полна. Токены — те же две валюты, что у
   занавеса награды дела (reward__xp / reward__coins), только мельче: здесь
   «Молодец!» уже сказано крупно, выплата — новость вторая. Значки рекорда
   и уровня — плашки gamecard__play: ребёнок читает их формой, не текстом. */
.gameend__pay {
  display: grid; justify-items: center; gap: var(--s2);
  width: 100%; margin-top: var(--s2);
  padding-top: var(--s2); border-top: 3px dotted var(--paper-deep);
}
.gameend__pay[hidden] { display: none; }
.gameend__xp {
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--fs-display) * 1);
  color: var(--success);
}
.gameend__coins {
  display: inline-flex; align-items: center; gap: .3em;
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--fs-display) * .7);
}
.gameend__coins .coin { width: .9em; height: .9em; }
.gameend__coins[hidden] { display: none; }
.gameend__record, .gameend__levelup {
  padding: 2px 14px; border-radius: var(--r-pill);
  font-weight: 800; font-size: var(--fs-caption);
}
.gameend__record { background: var(--amber-t); }
.gameend__levelup { background: var(--green-t); color: var(--green-p); }
/* Неоплаченная партия не молчит (вердикт критика): добрая строка вместо
   выплаты, тише токенов — новость «играй просто так», а не «мало получил». */
.gameend__free {
  color: var(--ink-muted); font-size: var(--fs-caption);
  text-align: center;
}
.gameend__actions { display: grid; gap: var(--s2); width: 100%; margin-top: var(--s3); }
.gameend__actions .btn { width: 100%; }

/* --------------------------- 24.8 игры: сетка ---------------------------- */

@media (max-width: 380px) {
  .gamegrid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2); }
  /* Не --s2 (8px): на 320px это ~2.4mm между карточками, меньше подушечки
   * пальца дошкольника. Промах по «ЛУНА» тогда наказывал бы ребёнка за
   * моторику, а не за знание. */
  .choices { gap: var(--s4); }
  /* Цикл 45, правка №2 критика-45: на узком кадре пара «ряд+поле»
   * отличий держит гэп s3, НЕ поднимаясь за .choices до s4 — иначе
   * клетка пятиколоночного раунда уходит ниже пола тапа 44px. */
  .diffrow { gap: var(--s3); }
  .choices--diffrow { gap: var(--s3); }
  .choice { min-height: 100px; }
  .swatch { width: 60px; height: 60px; }
  .memgrid { gap: var(--s2); }
}

@media (min-width: 640px) {
  .gamegrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .memgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .shell--game, .shell--games { max-width: 640px; }
}

/* ==========================================================================
   25. R3 — живой герой-компаньон.
   Один рисованный персонаж. Поза — атрибут data-pose на корне SVG,
   реквизит — data-prop, стадия — data-cape. Вся анимация CSS, ни одного
   движка; разметка приезжает с сервера и видна без JavaScript.
   ========================================================================== */

/* ------------------------------ 25.1 сцена -------------------------------- */

/* Размер героя — одна переменная: ширина задаётся здесь один раз, а каждое
 * место подменяет только --hero-size. Инлайн-SVG с viewBox без ширины растёт
 * до дефолта заменяемого элемента, поэтому «просто атрибут» тут не работает. */
/* Декорация и подсказчик, но never кнопка: не должен съедать тап,
 * предназначенный карточке с ответом. */
.hero { width: var(--hero-size); height: auto; overflow: visible; pointer-events: none; }

/* Своя полоса на экране игры: герой гуляет по ней, не залезая на карточки с
 * ответами. Полоса — часть раскладки, поэтому на любом телефоне герой не
 * может закрыть собой то, во что ребёнок должен ткнуть.
 * Высота больше самого героя ровно на высоту прыжка: иначе overflow:hidden
 * отрезает ему макушку на каждом правильном ответе. */
.heropark {
  position: relative;
  height: calc(var(--hero-size) * 1.1667 + 20px);
  margin-top: var(--s4);
  overflow: hidden;
}
.heropark .hero {
  position: absolute; left: 0; bottom: 0;
  transform: translateX(var(--hx, var(--s4)));
  transition: transform 900ms var(--ease-out);
}

/* SE-класс в Safari (320×481): единственный высотный media-запрос в файле.
   Аудит лестницы кадров (цикл 57) показал: раунд девяти игр не влезает в 481,
   ребёнок листает посреди партии (перевес 4…74px). Вместо девяти частных
   правил — одна экономия над доской: отступы s4→s2 (−40px суммарно) и герой
   108→76px (−37px): на кадре в треть детского роста он остаётся заметным,
   но перестаёт съедать фолд. Кнопки-ответы не тронуты: пол тапа не товар.
   Кадры 568 и выше условие не задевают. Блок стоит ПОСЛЕ базовых правил
   (.gamehead 1876, .gameprogress 1903, .gameprompt 1924, .gameboard 1931,
   .heropark 2421): при равной специфичности перезапись решает порядок. */
@media (max-height: 520px) {
  .gamehead, .gameprogress, .gameprompt, .heropark, .gameboard { margin-top: var(--s2); }
  .heropark { --hero-size: 76px; }
}

/* Дома у героя — шапка ребёнка и шапка игр. */
.kidhead__hero .hero { --hero-size: 88px; flex: 0 0 auto; }
.gameshub__head .hero { --hero-size: 64px; flex: 0 0 auto; margin-left: auto; }

/* Сцена: слот, в который герой пересаживается на время выступления. */
.heroscene { display: grid; justify-items: center; }
.heroscene .hero { --hero-size: 120px; }

.heroscene__caption {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-body); text-align: center; line-height: 1.25;
  animation: fade-in var(--dur-base) var(--ease-out);
}

/* Подсказка: герой показывает, карточка сама откликается кольцом. Ответ
 * подсвечивается, а не накрывается — ребёнок должен сам до него дотянуться. */
.choice.is-invited {
  animation: invite 900ms var(--ease-out) infinite alternate;
}
@keyframes invite {
  from { box-shadow: var(--sh-sticker), 0 0 0 0 color-mix(in srgb, var(--amber) 0%, transparent); }
  to { box-shadow: var(--sh-sticker), 0 0 0 7px color-mix(in srgb, var(--amber) 55%, transparent); }
}

/* ----------------------------- 25.2 окраска ------------------------------- */
/* Ни одного hex: цвет приходит токенами. Плащ и маска — стадия героя.
 * Шуба, морда и приметы — вид героя (R6), те же три переменные, что у знака
 * бренда .kitmark, плюс своя --kit-face: голова на тон светлее тела, и это
 * отношение обязано сохраняться у всех четырёх видов. */

.hero[data-cape="coral"] { --cape: var(--coral); }
.hero[data-cape="sky"] { --cape: var(--sky); }
.hero[data-cape="green"] { --cape: var(--green); }
.hero[data-cape="berry"] { --cape: var(--berry); }
.hero[data-cape="ink"] { --cape: var(--ink); }

/* Котёнок — герой, каким он был до R6: янтарь, круглые уши. Дефолт совпадает
 * с ним нарочно: .kitmark красится так же, и герой без вида не бывает. */
.hero {
  --kit-coat: var(--amber);
  --kit-face: var(--amber-h);
  --kit-muzzle: var(--paper);
  --kit-mark: var(--coral-d);
}
/* Лисёнок — рыжее «почти-коралловый»: чистый коралл занят плащом нулевой
 * стадии, и герой в плаще своего же цвета читался бы одним пятном. */
.hero[data-species="fox"] {
  --kit-coat: color-mix(in srgb, var(--coral) 74%, var(--paper));
  --kit-face: color-mix(in srgb, var(--coral) 52%, var(--paper));
  --kit-mark: var(--coral-p);
}
/* Зайчонок — пудрово-голубой: чистый --sky занят плащом первой стадии.
 * Хвостик — свой, а не бумажный: на белой карточке бумажный шарик даёт
 * 1.03:1 и просто исчезает. */
.hero[data-species="bunny"] {
  --kit-coat: color-mix(in srgb, var(--sky) 46%, var(--paper));
  --kit-face: color-mix(in srgb, var(--sky) 30%, var(--paper));
  --kit-muzzle: color-mix(in srgb, var(--sky) 26%, var(--surface));
  --kit-mark: var(--sky-p);
}
/* Дракончик — сине-зелёная шуба. Важно, что она СИНЯЯ, а не тёмная: плащ
 * второй стадии — --green, четвёртой — --ink, и тёмно-зелёная шуба против
 * них меняла только светлоту, то есть главный сигнал роста гас именно у
 * того вида, который растёт дольше всех. Смешиваем с --sky-p, а не с --ink:
 * тогда и зелёный, и чернильный плащ отличаются от шубы оттенком, а не
 * яркостью. Рожки и хвост-стрела красятся --kit-mark'ом, чтобы читались. */
.hero[data-species="dragon"] {
  --kit-coat: color-mix(in srgb, var(--green) 34%, var(--sky-p));
  --kit-face: color-mix(in srgb, var(--green) 16%, var(--sky-p));
  --kit-muzzle: var(--green-d);
  --kit-mark: var(--amber);
}

.hero .h-shadow { fill: color-mix(in srgb, var(--ink) 13%, transparent); }
.hero .h-root > g { fill: var(--kit-coat); }
.hero .h-root > g .h-belt { fill: var(--cape); }
.hero .h-cape path { fill: var(--cape); }
.hero .h-ear__in { fill: var(--kit-mark); }
.hero .h-head__shape { fill: var(--kit-face); }
.hero .h-mask { fill: var(--cape); }
.hero .h-eye circle:first-child { fill: var(--ink); }
.hero .h-eye__glint { fill: var(--surface); }
.hero .h-cheek { fill: var(--kit-mark); }
.hero .h-mouth {
  fill: none; stroke: var(--ink); stroke-width: 3;
  stroke-linecap: round;
}
.hero .h-mouth--open { fill: var(--ink); opacity: 0; }
.hero .h-badge { fill: var(--surface); }
.hero .h-badge__emoji {
  font-size: 19px; text-anchor: middle; dominant-baseline: central;
  /* Иначе эмодзи наследует --amber от корня: на платформе с одноцветными
   * эмодзи значок стадии станет янтарным на белом и исчезнет. */
  fill: var(--ink);
}
.hero .h-arm, .hero .h-leg { fill: var(--kit-coat); }

/* Морда и приметы вида (R6). Румянец красится --kit-mark'ом вместе с внутренним
 * ухом и носом: это одна «примета» вида, а не четыре отдельных решения. */
.hero .h-muzzle { fill: var(--kit-muzzle); }
.hero .h-nose { fill: var(--kit-mark); }
.hero .h-nostril { fill: color-mix(in srgb, var(--ink) 55%, transparent); }
.hero .h-tooth { fill: var(--surface); stroke: color-mix(in srgb, var(--ink) 22%, transparent); stroke-width: 1.5; }
.hero .h-whisker { fill: none; stroke: var(--ink); stroke-width: 1.8; stroke-linecap: round; opacity: .55; }
.hero .h-tail { fill: none; stroke: var(--kit-coat); stroke-width: 11; stroke-linecap: round; }
.hero .h-tail__mark { fill: none; stroke: var(--kit-mark); stroke-width: 4; stroke-linecap: round; }
.hero .h-tail__tip { fill: var(--kit-muzzle); }
.hero .h-tail__spade { fill: var(--kit-mark); }

/* Реквизит. Белый на белом не читается: сцена героя стоит на карточке того же
 * --surface, поэтому бумажным вещам нужен контур, а не только заливка. */
.hero .p-paper,
.hero .p-card {
  fill: var(--surface);
  stroke: color-mix(in srgb, var(--ink) 28%, transparent);
  stroke-width: 2.5;
}
.hero .p-ink { fill: var(--ink); }
.hero .p-sky { fill: var(--sky-p); }
.hero .p-green { fill: var(--green-h); }
.hero .p-leaf { fill: var(--green); }
.hero .p-coral { fill: var(--coral); }
.hero .p-amber { fill: var(--amber); }
.hero .p-berry { fill: var(--berry); }
.hero .p-bubble { fill: var(--surface); stroke: var(--sky-p); stroke-width: 2; }
.hero .p-stroke { fill: none; stroke: var(--ink); stroke-width: 2.5; stroke-linecap: round; }
.hero .p-stroke-thin { fill: var(--surface); stroke: var(--ink); stroke-width: 2; }
.hero .p-card__emoji { font-size: 25px; text-anchor: middle; dominant-baseline: central; fill: var(--ink); }
.hero .p-idea { font-size: 22px; text-anchor: middle; dominant-baseline: central; fill: var(--ink); }

.hero [data-art] { display: none; }
.hero[data-prop="brush"] [data-art="brush"],
.hero[data-prop="wash"] [data-art="wash"],
.hero[data-prop="water"] [data-art="water"],
.hero[data-prop="eat"] [data-art="eat"],
.hero[data-prop="read"] [data-art="read"],
.hero[data-prop="tidy"] [data-art="tidy"],
.hero[data-prop="pet"] [data-art="pet"],
.hero[data-prop="bed"] [data-art="bed"],
.hero[data-prop="bag"] [data-art="bag"],
.hero[data-prop="fold"] [data-art="fold"],
.hero[data-prop="help"] [data-art="help"],
.hero[data-prop="show"] [data-art="show"],
.hero[data-prop="idea"] [data-art="idea"],
.hero[data-prop="stroke"] [data-art="stroke"],
.hero[data-prop="blanket"] [data-art="blanket"] {
  display: block;
  animation: prop-in var(--dur-pop) var(--ease-spring) backwards;
}

/* --------------------------- 25.2b слоты вида ----------------------------- */
/* Уши, морда и хвост рисуются всегда, а показывает их data-species на корне —
 * тот же механизм, что у реквизита и одежды. Правило то же: селектор собирается
 * из вида на корне и вида в слоте, поэтому расхождение между ними — это не
 * ошибка рисования, а пустое место. tests/species.test.js держит словарь,
 * разметку и этот список вместе. */

.hero [data-ears], .hero [data-muzzle], .hero [data-tail] { display: none; }

.hero[data-species="fox"] [data-ears="fox"],
.hero[data-species="kitten"] [data-ears="kitten"],
.hero[data-species="bunny"] [data-ears="bunny"],
.hero[data-species="dragon"] [data-ears="dragon"],

.hero[data-species="fox"] [data-muzzle="fox"],
.hero[data-species="kitten"] [data-muzzle="kitten"],
.hero[data-species="bunny"] [data-muzzle="bunny"],
.hero[data-species="dragon"] [data-muzzle="dragon"],

.hero[data-species="fox"] [data-tail="fox"],
.hero[data-species="kitten"] [data-tail="kitten"],
.hero[data-species="bunny"] [data-tail="bunny"],
.hero[data-species="dragon"] [data-tail="dragon"] {
  display: block;
}

/* ------------------------------ 25.3 геометрия ---------------------------- */
/* transform-box: view-box делает origin координатой рисунка, а не проценты
 * рамки — плечо и бедро не поедут при изменении размера героя. */

.hero .h-flip, .hero .h-root, .hero .h-cape, .hero .h-body, .hero .h-head,
.hero .h-arm, .hero .h-hand, .hero .h-leg, .hero .h-props [data-art] {
  transform-box: view-box;
}
/* Смена позы без перехода — это телепорт: рука падает и вскидывается рывком
 * на каждом ответе ребёнка. `animation` ключевые кадры не анимирует, а вот
 * значение, которое проступает, когда анимация снята, — ловит. */
.hero .h-head, .hero .h-arm, .hero .h-leg {
  transition: transform 260ms var(--ease-out);
}
.hero .h-flip { transform-origin: 60px 70px; }
.hero .h-root { transform-origin: 60px 126px; }
.hero .h-cape { transform-origin: 60px 78px; }
.hero .h-body { transform-origin: 60px 118px; }
.hero .h-head { transform-origin: 60px 78px; }
.hero .h-arm--l { transform-origin: 27px 84px; }
.hero .h-arm--r { transform-origin: 93px 84px; }
.hero .h-leg--l { transform-origin: 50px 112px; }
.hero .h-leg--r { transform-origin: 70px 112px; }

/* --------------------------- 25.3b рост (R6) ------------------------------ */
/* Стадия — это не только цвет плаща и значок: это пропорции. Детёныш с большой
 * головой и коротким телом, взрослый герой — с длинным. Без этого пять стадий
 * были бы пятью перекрасками одного и того же роста, и «вырос» ребёнок бы не
 * увидел, а прочитал бы только по цифре уровня, которой он не знает.
 *
 * Масштаб живёт на ОБЁРТЫВАЮЩЕЙ группе (.h-grow), а не на .h-head и .h-body:
 * у тех transform занимает анимация позы, и она бы его затёрла. Обёртка ничего
 * не анимирует, поэтому оба трансформа складываются, а не спорят.
 * Голова растёт от шеи (60, 78) вверх, тело — от шеи (60, 74) вниз: шов между
 * ними не расходится ни на одной стадии. */
.hero .h-grow { transform-box: view-box; }
.hero .h-grow--head { transform-origin: 60px 78px; }
.hero .h-grow--body { transform-origin: 60px 74px; }

.hero[data-stage="0"] .h-grow--head { transform: scale(1.10); }
.hero[data-stage="1"] .h-grow--head { transform: scale(1.06); }
.hero[data-stage="2"] .h-grow--head { transform: scale(1); }
.hero[data-stage="3"] .h-grow--head { transform: scale(.96); }
.hero[data-stage="4"] .h-grow--head { transform: scale(.92); }

.hero[data-stage="0"] .h-grow--body { transform: scale(.92); }
.hero[data-stage="1"] .h-grow--body { transform: scale(.96); }
.hero[data-stage="2"] .h-grow--body { transform: scale(1); }
.hero[data-stage="3"] .h-grow--body { transform: scale(1.04); }
.hero[data-stage="4"] .h-grow--body { transform: scale(1.08); }

.hero[data-flip] .h-flip { transform: scaleX(-1); }

/* -------------------------------- 25.4 позы ------------------------------- */

/* Живёт всегда, в любой позе: дыхание, мигание, плащ. */
.hero .h-eye { transform-box: view-box; transform-origin: 60px 53px; animation: hero-blink 4.6s infinite; }
.hero .h-cape { animation: hero-cape 3.6s ease-in-out infinite; }
.hero[data-pose="idle"] .h-body { animation: hero-breathe 3.2s ease-in-out infinite; }
.hero[data-pose="idle"] .h-head { animation: hero-bob 3.2s ease-in-out infinite; }
.hero[data-pose="idle"] .h-arm--l { animation: hero-arm-idle 3.2s ease-in-out infinite; }
.hero[data-pose="idle"] .h-arm--r { animation: hero-arm-idle 3.2s ease-in-out infinite reverse; }

/* ------------------------ 25.4a нрав вида (R6) ---------------------------- */

/* Владелец просил к видам анимацию, и не зря: без неё «четыре вида» — одна
 * раскраска. Хвост качается в ЛЮБОЙ позе — поза занимает руки, голову и
 * корпус, а хвоста не касается. Голова и корпус — только в idle: в wave,
 * task и cheer их transform занят позой, и вторая animation затёрла бы её.
 * Origin — точка, где хвост растёт из спины, иначе мах отрывает хвост. */
.hero[data-species="fox"] [data-tail] {
  transform-box: view-box; transform-origin: 36px 116px;
  animation: tail-sway 2.6s ease-in-out infinite;
}
/* Котёнок не машет хвостом, а дёргает: короткие взмахи с паузой. */
.hero[data-species="kitten"] [data-tail] {
  transform-box: view-box; transform-origin: 36px 114px;
  animation: tail-twitch 3.4s ease-in-out infinite;
}
/* Помпон не машет — он сжимается, как у настоящего зайца на прыжке. */
.hero[data-species="bunny"] [data-tail] {
  transform-box: view-box; transform-origin: 24px 110px;
  animation: tail-puff 2.8s ease-in-out infinite;
}
.hero[data-species="bunny"][data-pose="idle"] .h-head { animation: bunny-listen 3.6s ease-in-out infinite; }
/* Дракончик парит: корпус висит в воздухе (в дыхание вшит подъём), стрела
 * хвоста ведёт за ним медленнее всех — большой зверь движется лениво. */
.hero[data-species="dragon"] [data-tail] {
  transform-box: view-box; transform-origin: 36px 114px;
  animation: tail-sway 4.4s ease-in-out infinite;
}
.hero[data-species="dragon"][data-pose="idle"] .h-body { animation: dragon-hover 4.4s ease-in-out infinite; }

@keyframes tail-sway {
  0%, 100% { transform: rotate(0deg); }
  50% { transform: rotate(-7deg); }
}
@keyframes tail-twitch {
  0%, 58%, 100% { transform: rotate(0deg); }
  64% { transform: rotate(-9deg); }
  72% { transform: rotate(3deg); }
  80% { transform: rotate(-6deg); }
}
@keyframes tail-puff {
  0%, 100% { transform: scale(1, 1); }
  50% { transform: scale(1.14, .86); }
}
/* Зайчонок «прислушивается»: наклон головы с паузой, подъём вшит вместо боба. */
@keyframes bunny-listen {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  40%, 62% { transform: translateY(1.6px) rotate(-4deg); }
}
/* У дракончика подъём и дыхание — одна animation: две на transform затёрли бы
 * друг друга целиком. */
@keyframes dragon-hover {
  0%, 100% { transform: scale(1, 1) translateY(0); }
  50% { transform: scale(1.015, 1.03) translateY(-1.6px); }
}

/* Приветствие. */
.hero[data-pose="wave"] .h-arm--r { animation: hero-wave 560ms ease-in-out infinite; }
.hero[data-pose="wave"] .h-arm--l { animation: hero-arm-idle 3.2s ease-in-out infinite; }
.hero[data-pose="wave"] .h-head { animation: hero-tilt 1.1s ease-in-out infinite alternate; }
.hero[data-pose="wave"] .h-mouth--open { opacity: 1; }

/* Праздник: прыжок, руки вверх, рот открыт. */
.hero[data-pose="cheer"] .h-root { animation: hero-jump 620ms var(--ease-spring) infinite; }
.hero[data-pose="cheer"] .h-arm--l { animation: hero-arm-up-l 620ms var(--ease-spring) infinite; }
.hero[data-pose="cheer"] .h-arm--r { animation: hero-arm-up-r 620ms var(--ease-spring) infinite; }
.hero[data-pose="cheer"] .h-cape { animation: hero-cape-flare 620ms var(--ease-spring) infinite; }
.hero[data-pose="cheer"] .h-mouth--open { opacity: 1; }

/* Шаг: идёт к цели — по экрану или к подсказке. */
.hero[data-pose="walk"] .h-leg--l { animation: hero-step 520ms ease-in-out infinite; }
.hero[data-pose="walk"] .h-leg--r { animation: hero-step 520ms ease-in-out infinite reverse; }
.hero[data-pose="walk"] .h-arm--l { animation: hero-swing 520ms ease-in-out infinite; }
.hero[data-pose="walk"] .h-arm--r { animation: hero-swing 520ms ease-in-out infinite reverse; }
.hero[data-pose="walk"] .h-root { animation: hero-bounce 260ms ease-in-out infinite; }

/* Думает и показывает: рука стоит в позе, а не анимируется к ней — при
 * prefers-reduced-motion жест остаётся читаемым. */
.hero[data-pose="think"] .h-arm--r { transform: rotate(-146deg); }
.hero[data-pose="think"] .h-arm--r { animation: hero-think 1.6s ease-in-out infinite; }
.hero[data-pose="think"] .h-head { animation: hero-tilt 1.6s ease-in-out infinite alternate; }

.hero[data-pose="point"] .h-arm--r { transform: rotate(-96deg); }
.hero[data-pose="point"] .h-arm--r { animation: hero-nudge 700ms ease-in-out infinite alternate; }
.hero[data-pose="point"] .h-head { transform: rotate(-4deg); }
.hero[data-pose="point"] .h-mouth--open { opacity: 1; }

/* Настоящее дело. Рука в кулаке держит реквизит, поэтому она и работает:
 * щётка и мыло — быстрые круги у морды, остальное — спокойная ноша. Без
 * реквизита (зарядка) герой прыгает и махает руками. */
.hero[data-pose="task"] .h-arm--r { animation: hero-carry 1.5s ease-in-out infinite alternate; }
.hero[data-pose="task"] .h-arm--l { animation: hero-arm-idle 2.4s ease-in-out infinite; }
.hero[data-pose="task"] .h-body { animation: hero-breathe 1.8s ease-in-out infinite; }
/* Щётка ведёт по самому рту: при +115° её головка ложится ровно в точку
 * (54, 66) — левый угол рта, а разворот на 20° прочёсывает его насквозь.
 * Мыло — другое: кран стоит справа у плеча, поэтому рука остаётся там же,
 * просто короче и ближе к телу. */
.hero[data-pose="task"][data-prop="brush"] .h-arm--r {
  animation: hero-brush 460ms ease-in-out infinite alternate;
}
.hero[data-pose="task"][data-prop="wash"] .h-arm--r {
  animation: hero-scrub 460ms ease-in-out infinite alternate;
}
.hero[data-pose="task"]:not([data-prop]) .h-arm--l { animation: hero-jack-l 560ms ease-in-out infinite; }
.hero[data-pose="task"]:not([data-prop]) .h-arm--r { animation: hero-jack-r 560ms ease-in-out infinite; }
.hero[data-pose="task"]:not([data-prop]) .h-root { animation: hero-jump 560ms var(--ease-spring) infinite; }
.hero[data-pose="task"]:not([data-prop]) .h-mouth--open { opacity: 1; }

/* Табличка с делом: герой держит её перед собой и слегка переступает. */
.hero[data-pose="show"] .h-arm--l { animation: hero-hold-l 900ms ease-in-out infinite alternate; }
.hero[data-pose="show"] .h-arm--r { animation: hero-hold-r 900ms ease-in-out infinite alternate; }
.hero[data-pose="show"] .h-root { animation: hero-flex 900ms ease-in-out infinite alternate; }
.hero[data-pose="show"] .h-mouth--open { opacity: 1; }

/* M5: спит. Глаза закрыты и не мигают — мигание поверх закрытых глаз читалось
 * бы дёрганьем. Дыхание медленнее бодрствующего: тот же такт hero-breathe,
 * растянутый вдвое, поэтому нового ключа дыхания не заводим. Голова чуть
 * клонится — единственное движение, которое отличает спящего от замершего. */
.hero[data-pose="sleep"] .h-eye { animation: none; transform: scaleY(.12); }
.hero[data-pose="sleep"] .h-body { animation: hero-breathe 6.4s ease-in-out infinite; }
.hero[data-pose="sleep"] .h-head { animation: hero-sleep-tilt 6.4s ease-in-out infinite; }
.hero[data-pose="sleep"] .h-arm--l,
.hero[data-pose="sleep"] .h-arm--r { animation: none; }
/* Под одеялом рот открыт ровно настолько, чтобы слышалось сопение, а не смех. */
.hero[data-pose="sleep"] .h-mouth--open { opacity: .55; }

/* ------------------------------ 25.5 реквизит ----------------------------- */

/* Реквизит в кулаке уже едет вместе с рукой — своя анимация ему не нужна,
 * кроме лейки: она льёт. Реквизит у тела живёт сам: книга качается, кубик
 * падает в коробку, яблоко летит ко рту.
 *
 * Селекторы здесь должны быть НЕ слабее, чем у правила выше (0,3,0): иначе
 * `animation` от prop-in перебьёт их, и реквизит замрёт после появления.
 * Своя жизнь идёт ПЕРВОЙ в списке, prop-in — последним: пока он активен, он
 * выигрывает transform (появление), а как только отыграл и снялся, вступает
 * собственная анимация реквизита. */
.hero[data-prop="water"] [data-art="water"] {
  animation: prop-pour 900ms ease-in-out infinite alternate,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}
.hero[data-prop="eat"] [data-art="eat"] {
  animation: prop-munch 620ms ease-in-out infinite,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}
.hero[data-prop="read"] [data-art="read"] {
  animation: prop-bob 1.6s ease-in-out infinite alternate,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}
.hero[data-prop="tidy"] [data-art="tidy"] {
  animation: prop-drop 780ms ease-in-out infinite,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}
.hero[data-prop="pet"] [data-art="pet"] {
  animation: prop-bob 700ms ease-in-out infinite alternate,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}
.hero[data-prop="show"] [data-art="show"] {
  animation: prop-bob 1.1s ease-in-out infinite alternate,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}
.hero[data-prop="idea"] [data-art="idea"] {
  animation: prop-glow 1.4s ease-in-out infinite alternate,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}
/* M5: реквизит заботы. Ладонь гладит — движется вдоль макушки, а не стоит;
 * одеяло дышит вместе с телом спящего, поэтому у него свой тихий такт. */
.hero[data-prop="stroke"] [data-art="stroke"] {
  animation: prop-stroke 1.8s ease-in-out infinite alternate,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}
.hero[data-prop="blanket"] [data-art="blanket"] {
  animation: prop-tuck 6.4s ease-in-out infinite,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}

/* ------------------------------- 25.6 такты ------------------------------- */

@keyframes hero-breathe {
  0%, 100% { transform: scale(1, 1); }
  50% { transform: scale(1.015, 1.035); }
}
@keyframes hero-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(1.6px); }
}
@keyframes hero-sleep-tilt {
  0%, 100% { transform: translateY(1px) rotate(-2.5deg); }
  50% { transform: translateY(1px) rotate(-1deg); }
}
@keyframes hero-blink {
  0%, 94%, 100% { transform: scaleY(1); }
  96% { transform: scaleY(.1); }
  98% { transform: scaleY(1); }
}
@keyframes hero-cape {
  0%, 100% { transform: skewX(0deg) scaleY(1); }
  50% { transform: skewX(2.5deg) scaleY(1.02); }
}
@keyframes hero-cape-flare {
  0%, 100% { transform: skewX(0deg) scale(1, 1); }
  40% { transform: skewX(-6deg) scale(1.05, 1.1); }
}
@keyframes hero-arm-idle {
  0%, 100% { transform: rotate(0deg); }
  50% { transform: rotate(4deg); }
}
@keyframes hero-wave {
  0%, 100% { transform: rotate(-138deg); }
  50% { transform: rotate(-104deg); }
}
@keyframes hero-tilt {
  from { transform: rotate(-5deg); }
  to { transform: rotate(5deg); }
}
@keyframes hero-jump {
  0%, 100% { transform: translateY(0) scaleY(1); }
  30% { transform: translateY(-13px) scaleY(1.03); }
  55% { transform: translateY(0) scaleY(.96); }
}
@keyframes hero-arm-up-l {
  0%, 100% { transform: rotate(150deg); }
  40% { transform: rotate(166deg); }
}
@keyframes hero-arm-up-r {
  0%, 100% { transform: rotate(-150deg); }
  40% { transform: rotate(-166deg); }
}
@keyframes hero-step {
  0%, 100% { transform: rotate(16deg); }
  50% { transform: rotate(-16deg); }
}
@keyframes hero-swing {
  0%, 100% { transform: rotate(-18deg); }
  50% { transform: rotate(18deg); }
}
@keyframes hero-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-3px); }
}
@keyframes hero-scrub {
  from { transform: rotate(-45deg); }
  to { transform: rotate(-70deg); }
}
@keyframes hero-brush {
  from { transform: rotate(103deg); }
  to { transform: rotate(123deg); }
}
@keyframes hero-carry {
  from { transform: rotate(-16deg); }
  to { transform: rotate(-26deg); }
}
@keyframes hero-think {
  0%, 100% { transform: rotate(-146deg); }
  50% { transform: rotate(-138deg); }
}
@keyframes hero-nudge {
  from { transform: rotate(-96deg); }
  to { transform: rotate(-108deg); }
}
@keyframes hero-jack-l {
  0%, 100% { transform: rotate(20deg); }
  50% { transform: rotate(158deg); }
}
@keyframes hero-jack-r {
  0%, 100% { transform: rotate(-20deg); }
  50% { transform: rotate(-158deg); }
}
@keyframes hero-hold-l {
  from { transform: rotate(-34deg); }
  to { transform: rotate(-26deg); }
}
@keyframes hero-hold-r {
  from { transform: rotate(34deg); }
  to { transform: rotate(26deg); }
}
@keyframes hero-flex {
  from { transform: rotate(-2deg) translateY(0); }
  to { transform: rotate(2deg) translateY(-2px); }
}

@keyframes prop-in {
  from { opacity: 0; transform: scale(.5); }
}
@keyframes prop-pour {
  from { transform: rotate(0deg); }
  to { transform: rotate(-20deg); }
}
@keyframes prop-bob {
  from { transform: translateY(0); }
  to { transform: translateY(-3.5px); }
}
@keyframes prop-munch {
  0%, 100% { transform: translate(0, 0) rotate(0deg); }
  40% { transform: translate(-14px, -12px) rotate(-14deg); }
  60% { transform: translate(-14px, -12px) rotate(-10deg) scale(.92); }
}
@keyframes prop-drop {
  0% { transform: translateY(-8px) rotate(-6deg); }
  55% { transform: translateY(0) rotate(0deg); }
  100% { transform: translateY(0) rotate(0deg); }
}
@keyframes prop-glow {
  from { transform: scale(.88) rotate(-6deg); opacity: .75; }
  to { transform: scale(1.08) rotate(6deg); opacity: 1; }
}
@keyframes prop-stroke {
  from { transform: translateX(-5px) rotate(-4deg); }
  to { transform: translateX(5px) rotate(4deg); }
}
@keyframes prop-tuck {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(1.4px); }
}

/* --------------------------- 25.7 reduced motion -------------------------- */
/* Общий запрет движения (§22) гасит тайминги. Гуляющую полосу прячем совсем —
 * там герой ничего не делает, кроме декорации. Жесты, которые несут смысл
 * (показывает, думает), остаются: они заданы статичным transform, а не
 * анимацией. */
@media (prefers-reduced-motion: reduce) {
  .heropark { display: none; }
  .hero .h-root, .hero .h-cape, .hero .h-body, .hero .h-head,
  .hero .h-arm, .hero .h-leg, .hero .h-eye, .hero [data-art] {
    animation: none !important;
  }
}

/* Спокойный режим (M6) гасит героя тем же списком селекторов: второй копии
 * списка нет только потому, что media-блок и атрибут в CSS не объединяются.
 * Полоска таймера спокойного экрана здесь не тронута — она ползёт от кода
 * и несёт время, то есть информацию, а не украшение. */
html[data-calm] .heropark { display: none; }
html[data-calm] .hero .h-root, html[data-calm] .hero .h-cape,
html[data-calm] .hero .h-body, html[data-calm] .hero .h-head,
html[data-calm] .hero .h-arm, html[data-calm] .hero .h-leg,
html[data-calm] .hero .h-eye, html[data-calm] .hero [data-art] {
  animation: none !important;
}

/* ==========================================================================
   26. R4 — монетки и комната героя.
   Валюта нарисована, а не эмодзи: 🪙 на части платформ приходит многоцветной
   картинкой, и цифра кошелька перестаёт быть главным. Вещь нарисована ОДИН
   раз (рамка 64×64 в src/views/room.js) и стоит и в сцене, и в карточке
   магазина, поэтому «в магазине нарисовано» и «в комнате стоит» не могут
   разъехаться. Показ вещи — атрибут data-own на корне сцены, тот же механизм,
   что data-prop у реквизита и data-wear у одежды.
   ========================================================================== */

/* --------------------------- 26.1 монетка и кошелёк ----------------------- */

.coin { width: 1em; height: 1em; display: block; }
.coin__edge { fill: var(--amber-p); }
.coin__disc { fill: var(--amber); }
.coin__glint { fill: var(--amber-h); }
.coin__star { fill: var(--amber-p); }

/* Кошелёк: цифра — главный, монетка — только метка валюты. Один размер
 * шрифта на всю систему: 1em монетка тянется за текстом и в шапке, и в цене. */
.purse {
  display: inline-flex; align-items: center; gap: .3em;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-body); line-height: 1;
  font-variant-numeric: tabular-nums;
}
.purse__n { color: var(--ink); }

/* В шапке комнаты и в шапке ребёнка кошелёк — самостоятельная плашка:
 * его должно быть видно с двух шагов, это счёт ребёнка, а не подпись. */
.purse--head {
  margin-left: auto;
  padding: var(--s2) var(--s3);
  border-radius: var(--r-pill);
  background: var(--amber-t);
  font-size: var(--fs-title);
}

/* В цене вещи цифра мельче: главное там — сама вещь. */
.shopitem__price .purse { font-size: var(--fs-caption); }
.shopitem__price .purse__n { color: var(--ink-muted); }

/* ------------------------------ 26.2 одежда ------------------------------- */
/* Ни одной строки JS: вещь всегда нарисована, а показывает её атрибут
 * data-wear на корне SVG. Краска — токены, как у всего героя. */

.hero [data-wear-art] { display: none; }
.hero[data-wear~="balloon"] [data-wear-art="balloon"],
.hero[data-wear~="scarf"] [data-wear-art="scarf"],
.hero[data-wear~="glasses"] [data-wear-art="glasses"],
.hero[data-wear~="crown"] [data-wear-art="crown"],
.hero[data-wear~="wings"] [data-wear-art="wings"],
.hero[data-wear~="santahat"] [data-wear-art="santahat"],
.hero[data-wear~="mittens"] [data-wear-art="mittens"],
.hero[data-wear~="cap"] [data-wear-art="cap"] {
  display: block;
  animation: prop-in var(--dur-pop) var(--ease-spring) backwards;
}

/* Появление — общее с реквизитом, своя жизнь у каждой вещи. Крылья и шарик
 * качаются вместе с героем, поэтому двигаются только две вещи, у которых
 * есть собственный смысл движения. Два правила ниже повторяют селектор
 * правила показа (0,3,0 — иначе оно победило бы их по специфичности, и
 * «своя жизнь» осталась бы мёртвым правилом, как это уже было с реквизитом),
 * а внутри `animation` постоянное движение стоит ПЕРВЫМ, prop-in — последним:
 * пока вход играет, он владеет transform, а закончив, отдает её обратно. */
.hero[data-wear~="wings"] [data-wear-art="wings"] {
  animation: wear-flap 1.2s ease-in-out infinite alternate,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
}
.hero[data-wear~="balloon"] [data-wear-art="balloon"] {
  animation: wear-sway 2.6s ease-in-out infinite alternate,
             prop-in var(--dur-pop) var(--ease-spring) backwards;
  transform-origin: 22px 79px;
}
/* Радость поднимает левую руку на 150° — шарик, живущий на этой руке,
 * уезжает при этом на живот. Прячем на время позы: герой без шарика читается
 * лучше, чем герой с шариком в пупке. */
.hero[data-pose="cheer"] [data-wear-art="balloon"] { display: none; }

.hero .w-balloon { fill: var(--coral); }
.hero .w-glint { fill: var(--coral-d); }
.hero .w-string { fill: none; stroke: var(--ink); stroke-width: 2; stroke-linecap: round; }
.hero .w-scarf { fill: var(--berry); }
.hero .w-scarf-line { fill: none; stroke: var(--berry-p); stroke-width: 2; stroke-linecap: round; }
.hero .w-lens { fill: var(--sky-t); }
.hero .w-rim { fill: none; stroke: var(--ink); stroke-width: 2.5; stroke-linecap: round; }
.hero .w-crown { fill: var(--amber); stroke: var(--amber-p); stroke-width: 1.5; }
.hero .w-gem { fill: var(--berry); }
.hero .w-pearl { fill: var(--surface); }
.hero .w-wing { fill: var(--sky-h); }
.hero .w-wing-line { fill: none; stroke: var(--sky-p); stroke-width: 2; stroke-linecap: round; }
/* Сезонные наборы (дизайн-лок 2026-09-05): шапочка и варежки — ягода с
 * бумажной опушкой, кепка — небо с козырьком потемнее и янтарной пуговкой.
 * Токены те же, что у всей одежды: сезонная вещь не имеет права выглядеть
 * гостем из другой палитры. Помпон шапочки — бумага с янтарным бликом. */
.hero .w-hat { fill: var(--berry); }
.hero .w-hat-trim { fill: var(--paper); }
.hero .w-pompom { fill: var(--paper); stroke: var(--amber-p); stroke-width: 1.5; }
.hero .w-mitten { fill: var(--berry); }
.hero .w-mitten-cuff { fill: var(--paper); }
.hero .w-cap { fill: var(--sky); }
.hero .w-cap-visor { fill: var(--sky-p); }
.hero .w-cap-btn { fill: var(--amber); }

/* ------------------------- 26.3 комната: сцена ---------------------------- */

.roomhead {
  display: flex; align-items: center; gap: var(--s3);
  flex-wrap: wrap;
  margin-top: var(--s4);
}
/* В шапке комнаты четыре элемента, и на узких телефонах им не хватало места:
 * заголовок в --fs-display (40px) выталкивал тумблер звука за край (проверено
 * рендером: документ 415px при окне 360px), а после уменьшения шрифта —
 * рисовался под кошельком (блок 47px при содержимом 116px). Поэтому заголовок
 * на ступеньку меньше, сжимается не уже самого длинного слова, а если и это
 * не влезает — переносится на свою строку. */
.roomhead .gameshub__titlebox { min-width: min-content; flex: 1 1 auto; }
.roomhead .gameshub__title { font-size: var(--fs-title); }

.roomstage { position: relative; margin: 0; }
.roomscene { display: block; width: 100%; height: auto; border-radius: var(--r-lg); }
/* Герой стоит на ковре: снизу 24% сцены — это пол (54 из 180), а 30% ширины —
 * ровно столько, чтобы ноги встали на y≈133, где лежит ковёр. Сдвиг влево от
 * центра освобождает правую стену под кроватку и полку. */
.roomstage__hero {
  position: absolute; left: 44%; bottom: 24%;
  width: 30%;
  transform: translateX(-50%);
}
/* Герой внутри комнаты задаётся шириной сцены, а не --hero-size. */
.roomstage__hero .hero { width: 100%; }

/* Бесплатная основа: она есть у всех, за неё не платят. */
.roomscene .rb-wall { fill: var(--sky-t); }
.roomscene .rb-floor { fill: var(--paper-deep); }
.roomscene .rb-plinth { fill: var(--line); }
.roomscene .rb-window { fill: var(--surface); }
.roomscene .rb-frame { fill: none; stroke: var(--line); stroke-width: 3; }
.roomscene .rb-sun { fill: var(--amber); }
.roomscene .rb-rug { fill: var(--amber-t); }
/* Шкаф — бесплатная мебель сцены (B5): та же древесная пара, что у коробки
 * и кровати, — дверцы светлее корпуса, ножки и ручки в цвет корпуса. */
.roomscene .rb-wardrobe-top,
.roomscene .rb-wardrobe-body,
.roomscene .rb-wardrobe-foot { fill: var(--amber-p); }
.roomscene .rb-wardrobe-door { fill: var(--amber-t); }

/* ----------------------- ROOM1: фоны комнаты (Update 11) ------------------
 * Вид из окна — четыре варианта в одной разметке; показывает только один,
 * по data-theme корня сцены. Фон меняет и стены с полом — но краской, не
 * геометрией: новых оттенков нет, все из палитры «Сказочного альбума», и
 * аудит B5 остаётся верен каждому фону сразу. Ночь — тёплая, чернильная:
 * холодный синий выпадал бы из комнаты, где вся мебель янтарная. */
.roomscene .rw { display: none; }
.roomscene[data-theme="day"] .rw--day,
.roomscene[data-theme="night"] .rw--night,
.roomscene[data-theme="winter"] .rw--winter,
.roomscene[data-theme="sea"] .rw--sea { display: block; }

.roomscene .rb-bird {
  fill: none; stroke: var(--ink-muted); stroke-width: 1.4; stroke-linecap: round;
}
.roomscene .rb-moonwin { fill: var(--amber); }
.roomscene .rb-star { fill: var(--amber-h); }
.roomscene .rb-snow { fill: var(--surface); }
.roomscene .rb-drift { fill: var(--surface); }
.roomscene .rb-wave {
  fill: none; stroke: var(--sky-h); stroke-width: 2; stroke-linecap: round;
}
.roomscene .rb-boat {
  fill: var(--coral); stroke: var(--coral-p); stroke-width: 1;
}

/* Ночь: чернильная стена, тёплый пол ночника, окно — небо с луной. */
.roomscene[data-theme="night"] .rb-wall { fill: var(--ink); }
.roomscene[data-theme="night"] .rb-floor { fill: var(--ink-muted); }
.roomscene[data-theme="night"] .rb-window { fill: var(--sky-p); }
.roomscene[data-theme="night"] .rb-plinth { fill: var(--berry-p); }
.roomscene[data-theme="night"] .rb-rug { fill: var(--sky-p); }

/* Зима: ледяная стена, окно в снегу — пол дома остаётся тёплым деревянным,
 * контраст и есть «зима за окном, дома тепло». */
.roomscene[data-theme="winter"] .rb-wall { fill: var(--sky-d); }
.roomscene[data-theme="winter"] .rb-window { fill: var(--sky-t); }

/* Море: стена цвета светлой воды, окно — волны и солнце. */
.roomscene[data-theme="sea"] .rb-wall { fill: var(--sky-d); }
.roomscene[data-theme="sea"] .rb-window { fill: var(--sky-h); }

/* ---------------- B6: редактор комнаты (Update 11) ------------------------ */

/* Карточка стоящей вещи: рисунок-имя слева, три дороги справа. Кнопки —
 * крупные, как все детские: стрелка и «в коробку» должны попадать с
 * первого раза. */
.editgrid {
  display: grid; gap: var(--s2);
}
.editcard {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s3);
  border-radius: var(--r-md); background: var(--surface);
  border: 2px solid var(--line);
}
.editcard.is-moved, .editcard.is-placed { border-color: var(--green); }
.editcard__art { flex: 0 0 auto; width: 56px; }
.editcard__art svg { display: block; width: 100%; height: auto; }
.editcard__name {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--fs-body); font-weight: 700; color: var(--ink);
}
.editcard__ways {
  display: flex; gap: var(--s2); align-items: center;
}
.editcard__act { display: contents; }
.editcard__btn {
  min-width: 52px; min-height: 52px; padding: 0 var(--s3);
  border: none; border-radius: var(--r-sm);
  background: var(--amber-t); color: var(--ink);
  font-family: inherit; font-size: 22px; font-weight: 800; line-height: 1;
  cursor: pointer;
}
.editcard__btn:active { transform: translateY(2px); }
.editcard__btn--store { background: var(--coral-t); font-size: var(--fs-small); }
.editcard__btn--place { background: var(--green-t); font-size: var(--fs-small); }

.editsec { margin-top: var(--s4); }
.editsec__title {
  margin: 0 0 var(--s2);
  font-size: var(--fs-small); font-weight: 800; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-muted);
}
.editempty {
  display: grid; gap: var(--s1); justify-items: center;
  padding: var(--s4) var(--s3);
  border-radius: var(--r-md);
  border: 2px dashed var(--line);
  text-align: center;
}
.editempty__emoji { font-size: 32px; }
.editempty__title { font-weight: 800; color: var(--ink); }
.editempty__hint { font-size: var(--fs-small); color: var(--ink-muted); }
.editsec__hint { margin: calc(-1 * var(--s1)) 0 var(--s2); font-size: var(--fs-small); color: var(--ink-muted); }

/* -------------------- ROOM2 (Update 15): тащат пальцем ---------------------
 *
 *  Двигаются только transform и opacity — перекомпоновок в кадре нет, это и
 *  есть цена 60 fps на дешёвом Android. Доезд (FLIP) — transition на transform:
 *  вещь едет из точки пальца в слот, а не прыгает. Тени и blur у поднятой вещи
 *  нет нарочно: filter заставляет пересобирать растр каждый кадр, и на слабом
 *  планшете именно он съедает кадр. «Поднято» читают масштаб, порядок (вещь
 *  в руке всегда последняя) и притухшие соседи. */

/* touch-action: none — иначе палец на сцене листает страницу, и pointermove
 * обрывается на первом же движении: жест не доезжает до прилипания. */
.roomstage--edit { touch-action: none; }
.roomscene--edit { touch-action: none; cursor: grab; }
.roomscene--edit.is-dragging { cursor: grabbing; }
.roomscene--edit .roomitem { cursor: grab; }
.roomscene--edit.is-dragging .roomitem:not(.is-drag) { opacity: .82; }
.roomitem.is-drag { cursor: grabbing; will-change: transform; }
/* Origin — угол сцены, как у transform-атрибута, которым вещь стоит: у SVG
 * по умолчанию он центр viewBox, и вещь съезжала бы на каждом масштабе —
 * при захвате (scale в руке) и при доезде. Та же причина, что у героя выше. */
.roomscene--edit .roomitem { transform-box: view-box; transform-origin: 0 0; }
.roomitem.is-snapping { transition: transform .32s cubic-bezier(.2, .8, .25, 1.05); }
.roomitem.is-leaving { transition: transform .3s ease-in, opacity .28s ease-in; }
.roomitem.is-boxed { display: none; }

/* Коробка — цель жеста «унести вниз», и она на экране всегда: у жеста обязано
 * быть место, куда тащить. Пустая — честно пустая: надпись вместо полки. */
.roombox {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  min-height: 76px; margin-top: var(--s3); padding: var(--s2) var(--s3);
  border: 2px dashed var(--line); border-radius: var(--r-md);
  background: var(--surface);
}
.roombox.is-over { border-color: var(--green); background: var(--green-t); }
.roombox__mark { flex: 0 0 auto; font-size: 26px; line-height: 1; }
.roombox__empty { font-size: var(--fs-small); color: var(--ink-muted); }
.boxchip {
  display: grid; gap: var(--s1); justify-items: center; min-width: 72px;
  min-height: 56px; padding: var(--s1) var(--s2);
  border: 2px solid var(--line); border-radius: var(--r-md); background: var(--paper);
  font-family: inherit; cursor: grab;
}
.boxchip:active { transform: translateY(2px); }
/* Чип вещи, которая стоит в комнате: вещь в сцене, и двух рисунков не нужно.
 * Унесённая в коробку показывает чип обратно — иначе её уже не достать. */
.boxchip.is-out { display: none; }
.boxchip__art { width: 40px; }
.boxchip__art svg { display: block; width: 100%; height: auto; }
.boxchip__name { font-size: var(--fs-small); font-weight: 700; color: var(--ink); }

/* «Готово» — вся расстановка одним POST, «Отмена» — уход без записи. */
.roomeditbar { display: flex; gap: var(--s2); margin-top: var(--s3); }
.roomeditbar__cancel,
.roomeditbar__done {
  display: grid; place-items: center; min-height: 56px; padding: var(--s1) var(--s3);
  border-radius: var(--r-pill); font-weight: 800; font-size: var(--fs-body);
  text-decoration: none; cursor: pointer;
}
.roomeditbar__cancel {
  flex: 0 0 auto; background: var(--surface); color: var(--ink);
  border: 2px solid var(--line);
}
.roomeditbar__done {
  flex: 1 1 auto; border: none; background: var(--green); color: var(--surface);
  font-family: inherit;
}
.roomeditbar__done:active { transform: translateY(2px); }

/* Фоны: карточка — мини-комната, у включённого нет кнопки, только знак. */
/* minmax(0, 1fr), а не 1fr: минимальная ширина трека — минимальное
 * содержимое карточки, и при 320 две карточки выталкивало за край
 * (урок gamehead/min-width: 0). */
.themegrid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2);
}
.themecard {
  /* minmax(0, 1fr), а не явный трек: единственная колонка карточки раньше
   * шла по min-content самого широкого ребёнка (кнопка/имя — 134px), вылезая
   * за саму карточку 132px по обе стороны — гейт лестницы 55-го поймал +6px
   * на 320 (редактор комнаты, 4 виновника). Тот же урок gamehead/min-width:
   * 0, что и у .themegrid выше. */
  display: grid; grid-template-columns: minmax(0, 1fr);
  gap: var(--s1); justify-items: center;
  min-width: 0;
  padding: var(--s2);
  border-radius: var(--r-md); background: var(--surface);
  border: 2px solid var(--line); text-align: center;
}
.themecard > * { min-width: 0; max-width: 100%; }
.themecard--on { border-color: var(--green); background: var(--green-t); }
.themecard__art { width: 72px; }
.themecard__art svg { display: block; width: 100%; height: auto; }
.themecard__name { font-size: var(--fs-small); font-weight: 700; color: var(--ink); }
.themecard__mark {
  font-size: 18px; font-weight: 800; color: var(--green);
}
.themecard__btn {
  border: none; border-radius: var(--r-pill);
  /* Пол тапа (было 41px) и потолок ширины: кнопка не раздвигает карточку. */
  min-height: 44px; max-width: 100%;
  padding: var(--s1) var(--s3);
  background: var(--green); color: var(--surface);
  font-family: inherit; font-size: var(--fs-small); font-weight: 800;
  cursor: pointer;
}
.themecard__btn:active { transform: translateY(2px); }

/* Мини-комната карточки фона: одна разметка, четыре краски — по ключу фона
 * на обёртке (data-art-theme), тем же жестом, каким сцена красится data-theme. */
.mt-frame, .mt-bed { fill: var(--amber-p); }
[data-art-theme="day"] .mt-wall { fill: var(--sky-t); }
[data-art-theme="day"] .mt-floor { fill: var(--paper-deep); }
[data-art-theme="day"] .mt-win { fill: var(--surface); }
[data-art-theme="day"] .mt-view { fill: var(--amber); }
[data-art-theme="night"] .mt-wall { fill: var(--ink); }
[data-art-theme="night"] .mt-floor { fill: var(--ink-muted); }
[data-art-theme="night"] .mt-win { fill: var(--sky-p); }
[data-art-theme="night"] .mt-view { fill: var(--amber); }
[data-art-theme="winter"] .mt-wall { fill: var(--sky-d); }
[data-art-theme="winter"] .mt-floor { fill: var(--paper-deep); }
[data-art-theme="winter"] .mt-win { fill: var(--sky-t); }
[data-art-theme="winter"] .mt-view { fill: var(--surface); }
[data-art-theme="sea"] .mt-wall { fill: var(--sky-d); }
[data-art-theme="sea"] .mt-floor { fill: var(--paper-deep); }
[data-art-theme="sea"] .mt-win { fill: var(--sky-h); }
[data-art-theme="sea"] .mt-view { fill: var(--amber); }
[data-art-theme] .mt-view--dot { fill: var(--ink-muted); }

/* Купленные вещи. Показ — атрибут data-own на корне сцены. */
.roomscene [data-room-art] { display: none; }
.roomscene[data-own~="garland"] [data-room-art="garland"],
.roomscene[data-own~="picture"] [data-room-art="picture"],
.roomscene[data-own~="rocket"] [data-room-art="rocket"],
.roomscene[data-own~="shelf"] [data-room-art="shelf"],
.roomscene[data-own~="lamp"] [data-room-art="lamp"],
.roomscene[data-own~="plant"] [data-room-art="plant"],
.roomscene[data-own~="toybox"] [data-room-art="toybox"],
.roomscene[data-own~="bed"] [data-room-art="bed"],
.roomscene[data-own~="ball"] [data-room-art="ball"],
.roomscene[data-own~="fish"] [data-room-art="fish"],
.roomscene[data-own~="lantern"] [data-room-art="lantern"],
.roomscene[data-own~="moon"] [data-room-art="moon"],
.roomscene[data-own~="butterfly"] [data-room-art="butterfly"],
.roomscene[data-own~="tree"] [data-room-art="tree"],
.roomscene[data-own~="flamingo"] [data-room-art="flamingo"],
.roomscene[data-own~="bucket"] [data-room-art="bucket"],
.roomscene[data-own~="starfish"] [data-room-art="starfish"],
.roomscene[data-own~="bell"] [data-room-art="bell"],
.roomscene[data-own~="umbrella"] [data-room-art="umbrella"],
.roomscene[data-own~="mushroom"] [data-room-art="mushroom"],
.roomscene[data-own~="pumpkin"] [data-room-art="pumpkin"],
.roomscene[data-own~="acorn"] [data-room-art="acorn"] {
  display: block;
  animation: prop-in var(--dur-pop) var(--ease-out) backwards;
}

/* Краска вещей. Классы общие для всех девяти, а вещь всегда лежит внутри
 * группы [data-room-art] — и в сцене, и в карточке магазина, — поэтому один
 * список красит оба места: одна палитра — одна комната. */
[data-room-art] .ri-wood { fill: var(--amber-p); }
[data-room-art] .ri-amber { fill: var(--amber); }
[data-room-art] .ri-soft { fill: var(--amber-t); }
[data-room-art] .ri-glow { fill: var(--amber-h); }
[data-room-art] .ri-paper {
  fill: var(--surface);
  stroke: color-mix(in srgb, var(--ink) 22%, transparent);
  stroke-width: 1.5;
}
[data-room-art] .ri-sky { fill: var(--sky-p); }
[data-room-art] .ri-leaf { fill: var(--green); }
[data-room-art] .ri-stem {
  fill: none; stroke: var(--green); stroke-width: 3; stroke-linecap: round;
}
[data-room-art] .ri-coral { fill: var(--coral); }
[data-room-art] .ri-berry { fill: var(--berry); }
[data-room-art] .ri-glass { fill: var(--sky-t); stroke: var(--sky-p); stroke-width: 2.5; }
[data-room-art] .ri-water { fill: var(--sky-h); }
[data-room-art] .ri-line {
  fill: none; stroke: var(--ink-faint); stroke-width: 2.5; stroke-linecap: round;
}

.roomhint {
  margin-top: var(--s4); padding: var(--s3) var(--s4);
  border-radius: var(--r-md); background: var(--amber-t);
  font-size: var(--fs-body); text-align: center;
}

/* M5/HERO2: стили заботы переехали целиком в §99 в конце файла — меню из пяти
 * адресатов и последовательности героя живут одним блоком (ветка cycle57-hero2). */

/* --------------------------- 26.4 комната: магазин ------------------------ */

/* M4: наборы вместо полок по роду вещи. Заголовок набора — одна строка:
 * картинка, имя и счётчик. Ребёнок собирает не «одежду», а «Космос», поэтому
 * счётчик стоит справа, где его видно, не двигая карточки. */
.shopset { margin-top: var(--s5); }
.shopset__head {
  display: flex; align-items: center; gap: var(--s2);
  font-size: var(--fs-title); margin-bottom: var(--s3);
}
.shopset__emoji { font-size: 28px; }
.shopset__name { font-weight: 700; }
.shopset__count {
  margin-left: auto; padding: 2px var(--s3);
  border-radius: var(--r-pill);
  background: var(--paper-deep); color: var(--ink-muted);
  font-size: var(--fs-caption); font-weight: 700;
}
/* «Почти собрано» видно ещё до того, как ребёнок умеет читать счётчик:
 * жёлтый — «осталась одна», зелёный — «всё». Никакой спешки в цвете нет —
 * это состояние, а не обещание. */
.shopset__count--almost { background: var(--amber-t); color: var(--ink); }
.shopset__count--done { background: var(--green-t); color: var(--green-p); }
.shopset__almost {
  margin-top: var(--s3); padding: var(--s3) var(--s4);
  border-radius: var(--r-md); background: var(--amber-t);
  text-align: center; font-weight: 700;
}
.shopset__done {
  margin-top: var(--s3); padding: var(--s3) var(--s4);
  border-radius: var(--r-md); background: var(--green-t); color: var(--green-p);
  text-align: center; font-weight: 700;
}

.shopgrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3); }

.shopitem {
  position: relative;
  display: grid; gap: var(--s1); justify-items: center; text-align: center;
  padding: var(--s4) var(--s3);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 4px solid var(--surface);
  box-shadow: var(--sh-sticker);
  animation: sticker-in var(--dur-slow) var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 45ms);
}

.shopitem__art { display: block; width: 76px; height: 76px; }
.shopitem__art svg { width: 100%; height: 100%; overflow: visible; }
.shopitem__name {
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
  line-height: 1.2;
}

/* Двухкасание: первый тап спрашивает, второй покупает. Та же механика, что
 * у «Готово!» на карточке дела, — ребёнку не приходится учить второй жест. */
.shopitem__btn {
  width: 100%; min-height: 48px; border-radius: var(--r-pill);
  background: var(--green); color: var(--surface);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
  transition: transform var(--dur-fast) var(--ease-spring), background-color var(--dur-fast) var(--ease-out);
}
.shopitem__btn::after { content: "Купить"; }
.shopitem.is-arming .shopitem__btn { background: var(--coral); }
.shopitem.is-arming .shopitem__btn::after { content: "Точно купить?"; }
.shopitem.is-arming { animation: pop var(--dur-pop) var(--ease-spring); }
.shopitem__btn:active { transform: scale(.97); }
/* Карточка отвечает, пока говорит: в шумной комнате тап без ответа на экране
 * читается ребёнком как сломанная вещь, а не как «слушай». */
.shopitem.is-speaking { border-color: var(--green); }
.shopitem.is-speaking .shopitem__art { transform: scale(1.06); }

/* Куплено: вещь остаётся на полке видимой — ребёнок должен видеть, что у него
 * есть. Покупки не конфискуются и не исчезают с витрины. */
.shopitem--owned { background: var(--green-t); border-color: var(--green-t); }
.shopitem--owned .shopitem__art { opacity: 1; }

/* M4: слот подарка. Это кнопка, а не форма, поэтому кнопочные сбросы нужны
 * здесь: остальная карточка — форма и наследует шрифт страницы сама. Пунктир
 * отделяет загадку от товаров — то, чего нельзя купить, не должно выглядеть
 * как то, что можно. */
.shopitem--secret {
  font: inherit; color: inherit; cursor: pointer;
  background: var(--paper-deep); border-color: var(--paper-deep);
  border-style: dashed;
}
.shopitem--secret:active { transform: scale(.98); }
.shopitem--secret .shopitem__art { display: grid; place-items: center; }
.shopitem__mystery {
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--fs-display) * 1.4); color: var(--ink-muted);
}
/* Подарок, который уже получен: та же зелёная карточка, что у купленного,
 * но строка состояния говорит правду — это подарок, а не покупка. */
.shopitem--gifted { background: var(--green-t); border-color: var(--green-t); }
.shopitem__state--gift { background: var(--green); color: var(--surface); }
/* Сезонная вещь без подписки (дизайн-лок 2026-09-05): карточка не прячется,
 * но и не притворяется товаром — пунктир, как у загадки, приглушённый
 * рисунок и спокойная пометка вместо цены. Тот же жест, что у платного вида
 * на экране выбора героя. */
.shopitem--locked {
  font: inherit; color: inherit; cursor: pointer;
  background: var(--paper-deep); border-color: var(--paper-deep);
  border-style: dashed;
}
.shopitem--locked:active { transform: scale(.98); }
.shopitem--locked .shopitem__art { opacity: .55; }
.shopitem__state--sub {
  background: var(--surface); color: var(--ink-muted);
}
.shopitem__state {
  min-height: 48px; display: inline-flex; align-items: center;
  padding: var(--s2) var(--s4); border-radius: var(--r-pill);
  background: var(--paper-deep);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-caption); color: var(--ink-muted);
}
.shopitem--owned .shopitem__state { background: var(--green); color: var(--surface); }

/* Копилка: спокойная констатация, а не давление. Ни таймера, ни «успей»,
 * ни зачёркнутой цены — только честное «сколько ещё нужно». */
.shopitem--short .shopitem__art { opacity: .45; }
.shopitem--short .shopitem__name { color: var(--ink-muted); }
.shopitem--short .shopitem__state { background: var(--amber-t); color: var(--ink); }

/* R9: надеть и снять. Галочка в углу — тот же знак «это активно», что у
 * выбранного вида: нечитающий ребёнок отличает надетое по жёлтому кругу, а не
 * по слову на кнопке. Кнопка серым в покое и зелёная на герое — её цвет
 * повторяет состояние, чтобы тап и результат были одной краской. */
.shopitem__mark {
  position: absolute; top: calc(var(--s2) * -1); right: calc(var(--s2) * -1);
  width: 32px; height: 32px; display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--amber); color: var(--ink);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
  box-shadow: var(--sh-1);
}
.shopitem__btn--wear { background: var(--paper-deep); color: var(--ink); }
.shopitem__btn--wear::after { content: 'Надеть'; }
.shopitem--worn .shopitem__btn--wear { background: var(--green); color: var(--surface); }
.shopitem--worn .shopitem__btn--wear::after { content: 'Снять'; }
.shopitem.is-worn { animation: pop var(--dur-pop) var(--ease-spring); }

/* ------------------------------ 26.5 движение ----------------------------- */

@keyframes wear-flap {
  from { transform: rotate(-4deg); }
  to { transform: rotate(4deg); }
}
@keyframes wear-sway {
  from { transform: rotate(-5deg); }
  to { transform: rotate(5deg); }
}

/* Карточка, которую только что купили, отмечается на месте: страница
 * перезагружается после покупки, и без метки ребёнок не находит свою вещь. */
.shopitem.is-bought { animation: pop var(--dur-pop) var(--ease-spring); }

@media (prefers-reduced-motion: reduce) {
  .roomscene [data-room-art],
  .hero [data-wear-art] { animation: none !important; }
  .shopitem.is-bought { animation: none; }
  .shopitem.is-worn { animation: none; }
  .shopitem.is-arming { animation: none; }
  /* M1: предпраздник без движения. Голос и пунктир на цене остаются — они не
   * движение, а состояние; исчезают только прыжки. */
  .kidtask.is-celebrating,
  .kidtask.is-celebrating .kidtask__reward { animation: none; }
  .kidtask.is-returned { animation: none; }
}

@media (min-width: 640px) {
  .shopgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .roomstage { margin: 0 auto; max-width: 560px; }
}

/* ==========================================================================
   27. R6 — выбор вида героя.
   Выбор делает тот, кто не читает: карточка вида — это сам герой, машущий
   лапой, плюс кнопка звука. Ни одного диалога подтверждения: вид ничего не
   стоит, меняется тем же тапом обратно, и перерисовка идёт с сервера, поэтому
   выбор работает и без JavaScript.
   ========================================================================== */

/* Вопрос экрана. Это кнопка, а не заголовок: ребёнок слышит задание, а не
 * читает его. Значок звука — в углу, а не в строке с текстом: урок R7 — в
 * детском режиме --fs-title равен 28px, строка в два слова переносится на две
 * строки, и значок в строке отнимает у текста ширину, которой и так нет.
 * Она НЕ класс .say: тот прибит к углу (absolute, top/right −8px), и кнопка
 * во всю ширину уезжала бы вверх, наезжая на заголовок экрана. */
.species-ask {
  position: relative; width: 100%;
  min-height: 56px; padding: var(--s3) var(--s6) var(--s3) var(--s4);
  border: 2px dashed var(--line); border-radius: var(--r-lg);
  background: var(--surface);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-title); color: var(--ink);
  text-align: left;
}
.species-ask:active { transform: scale(.98); }
.species-ask__text { display: block; min-width: 0; }
.species-ask__icon {
  position: absolute; top: var(--s2); right: var(--s2);
  font-size: 20px; color: var(--ink-muted);
}
.species-ask.is-speaking { border-color: var(--green); color: var(--green); }
.species-ask.is-speaking .species-ask__icon { color: var(--green); }

.specgrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3); }

/* Карточка вида — форма из двух кнопок: большая выбирает, полоска снизу
 * говорит. Кнопка звука — во всю ширину низа, а не в углу: углы карточки
 * занимает рисунок героя (хвост и шарик он рисует за своим viewBox), и в углу
 * кнопка ложилась бы на ухо или на имя. Целью во всю ширину попасть проще
 * всего, а это единственное действие, которое нужно двухлетке. */
.speccard {
  position: relative;
  display: grid; justify-items: center; gap: var(--s2);
  padding: var(--s3); border: 2px solid var(--line); border-radius: var(--r-lg);
  background: var(--surface);
}
.speccard__pick {
  display: grid; justify-items: center; gap: var(--s2); width: 100%;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-label); color: var(--ink);
  /* B1: карточка стала ссылкой, а осталась картинкой — подчёркивание от <a>
   * читалось как «текст-ссылка», и имена видов красились по-разному: у героя,
   * которого ткнуть нельзя, его не было. */
  text-decoration: none;
}
/* 84px, а не больше: карточка на 320px — 106px внутри, и герой обязан
 * остаться внутри неё, не залезая на кнопку звука. */
.speccard__pick .hero { --hero-size: 84px; }
.speccard__say {
  width: 100%; min-height: 44px;
  display: grid; place-items: center;
  border-radius: var(--r-pill); background: var(--paper-deep);
  font-size: 20px; color: var(--ink-muted);
  transition: transform var(--dur-fast) var(--ease-spring), background-color var(--dur-fast) var(--ease-out);
}
.speccard__say:active { transform: scale(.96); }
/* Свой ответ, а не чужой: эта полоска — не .say (тот прибит к углу карточки
 * и наезжает на героя), поэтому зеленеет сам, пока говорит. */
.speccard__say.is-speaking { background: var(--green); color: var(--surface); }
.speccard.is-speaking { border-color: var(--green); }

/* Выбранный вид: толстое кольцо и галочка — знак, который читается без
 * чтения. Подписи «выбрано» нет: неграмотному ребёнку она ничего не говорит. */
.speccard--chosen {
  border-color: var(--amber);
  box-shadow: var(--sh-sticker), inset 0 0 0 3px var(--amber);
}
.speccard__mark {
  position: absolute; top: calc(var(--s2) * -1); right: calc(var(--s2) * -1);
  width: 32px; height: 32px; display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--amber); color: var(--ink);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
  box-shadow: var(--sh-1);
}

@media (min-width: 640px) {
  .specgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* B1: шаг подтверждения. Один кандидат на весь экран вместо сетки: выбор для
 * нечитающего ребёнка — «посмотреть на него большого и решить», а не сравнение
 * четырёх подписей. Кнопка одна и на всю ширину — второй большой цели нет,
 * а «выбрать другого» остаётся тихой ссылкой под ней. */
.speconfirm {
  display: grid; justify-items: center; gap: var(--s3);
  padding: var(--s5) var(--s3) var(--s4);
  border: 2px solid var(--line); border-radius: var(--r-lg);
  background: var(--surface);
}
.speconfirm .hero { --hero-size: 152px; }
.speconfirm__name { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-title); }
.speconfirm__no {
  display: inline-flex; align-items: center; min-height: 44px;
  color: var(--ink-muted); font-size: var(--fs-caption);
}

/* ==========================================================================
   28. M7 — общая банка недели.
   Одно общее дело на всю семью: каждое зачтённое дело любого ребёнка кладёт
   в банку звёздочку, полная банка платит каждому герою сразу. Вклад одного
   здесь не виден отдельно от вклада другого — и именно поэтому банка не
   таблица лидеров. Тепло (янтарное), а не голубое: голубой в этом языке
   значит «подсказка», а банка — про награду.
   ========================================================================== */

/* Банка — рисованная, одна на все экраны: стрип ребёнка, строка родителя и
 * занавес награды рисуют её одинаково, различается только размер, который
 * задаёт родительский компонент. Эмодзи 🍯 принесло бы свой цвет и свою
 * форму в систему, у которой уже есть монетка и огоньки. Налив — высотой
 * от --p: информация об уровне не должна зависеть от анимаций, которые
 * правила тишины гасят целиком. */
.qjar { position: relative; display: block; flex: none; }
.qjar__lid {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 52%; height: 14%;
  border-radius: var(--r-pill);
  background: var(--ink-muted);
}
.qjar__glass {
  position: absolute; inset: 16% 0 0;
  overflow: hidden;
  border: 3px solid var(--ink-muted);
  border-radius: 22% 22% 28% 28% / 18% 18% 24% 24%;
  background: var(--surface);
}
.qjar__fill {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: var(--p, 0%);
  background: linear-gradient(160deg, var(--amber-h), var(--coral));
}

/* Стрип ребёнка. Первая строка секции дел — над тем, что её наполняет.
 * Кнопка целиком: тап объясняет голосом, как у огоньков и новости дня.
 * Тёплый, но бледный: это не «Готово!», и спорить с главной кнопкой экрана
 * ему не положено.
 *
 * D5: стрип переехал в комнату, и без родной сетки секции дел кнопка
 * сжималась до ширины текста и прилипала к новости дня (223px и нулевой
 * зазор на 358px — измерено). Ширина и отступ те же, что у соседних
 * тихих полос комнаты: одна колонка — один ритм. */
.kidquest {
  display: flex; align-items: center; gap: var(--s3);
  width: 100%; margin-top: var(--s5);
  padding: var(--s3) var(--s4);
  border: 3px solid var(--amber-d); border-radius: var(--r-lg);
  background: var(--amber-t);
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring),
              border-color var(--dur-fast) var(--ease-out);
}
.kidquest:active { transform: scale(.98); }
.kidquest .qjar { width: 38px; height: 48px; }
.kidquest__text {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
}
/* Монетка появляется только у полной банки: уровень в банке говорит «как
 * дела», монетка — «награда здесь». Серого «почти-монетки» не бывает. */
.kidquest__coin { flex: none; font-size: 26px; }
.kidquest__go {
  flex: none; display: grid; place-items: center;
  width: 44px; height: 44px; border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink);
}
.kidquest.is-speaking { border-color: var(--green); }
.kidquest.is-speaking .kidquest__go { background: var(--green-t); }

/* Полная банка держится полной до конца недели и говорит об этом чуть громче:
 * рамка тепла стала насыщеннее, а фон — плотнее. Тише, чем любая кнопка
 * действия, громче, чем пустая банка: это единственное состояние банки,
 * которое ребёнок должен заметить сам. */
.kidquest--full { border-color: var(--amber-p); background: var(--amber-d); }

/* Свой момент банки для каждого героя (стрип празднует того, кто не был
 * автором дела). Один раз в неделю, без занавеса и без конфетти: момент
 * должен случиться, не должен выгнать задание с экрана и не должен спорить
 * голосом с «Готово!». Спокойный режим глушит движение — фразу и монетку он
 * не трогает: это информация, а не украшение. */
.kidquest--party { animation: kidquest-party 1.1s var(--ease-out) 3; }
.kidquest--party .kidquest__coin { animation: coin-pop .5s var(--ease-spring) .35s; }

@keyframes kidquest-party {
  0%, 100% { transform: none; }
  35% { transform: scale(1.03); }
}
@keyframes coin-pop {
  from { transform: scale(0) rotate(-40deg); }
  to { transform: none; }
}

/* Строка родителя. Те же цифры, что и у взрослого счётчика заданий, и никакой
 * разбивки по детям: разобрать банку на вклады — построить из неё соревнование.
 * Ставится в «Героях», а не в свёрнутой настройке: в неё заглядывают каждый
 * день, а не раз в месяц. */
.questline {
  display: flex; align-items: flex-start; gap: var(--s3);
  margin-top: var(--s4); padding: var(--s3);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--paper);
}
.questline .qjar { width: 30px; height: 38px; }
.questline .qjar__glass { border-width: 2px; }
.questline__body { flex: 1 1 auto; min-width: 0; display: grid; gap: var(--s2); }
.questline__title {
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
  line-height: 1.2;
}
.questline__track {
  height: 10px; border-radius: var(--r-pill);
  background: var(--paper-deep); overflow: hidden;
}
.questline__fill {
  display: block; height: 100%; width: var(--p, 0%);
  border-radius: var(--r-pill);
  background: linear-gradient(160deg, var(--amber-h), var(--coral));
}
.questline__hint { color: var(--ink-muted); font-size: var(--fs-label); line-height: 1.45; }

/* Банка в занавесе награды: та же, что на стрипе, только полная и маленькая. */
.reward__quest {
  display: flex; align-items: center; justify-content: center; gap: var(--s2);
}
.reward__quest .qjar { width: 26px; height: 34px; }
.reward__quest .qjar__glass { border-width: 2px; }
.reward__quest-name {
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
}

/* ==========================================================================
   30. M6 — спокойный режим: одно дело на экране.
   Экран для ребёнка, которого список парализует: одна карточка, герой рядом,
   таймер — и больше ничего. Всё здесь тихое не потому, что не успели
   раскрасить: тишина и есть функция экрана.
   ========================================================================== */

/* Значки режима рисованные, а не эмодзи: у эмодзи свой цвет, и он выпадает из
 * системы рядом с токенами — та же причина, по которой монетка нарисована. */
.miniicon { display: block; width: 1em; height: 1em; fill: currentColor; }

/* Вход в режим — родительская вещь на детском экране, поэтому она тише любой
 * плитки: цвет бумаги, подпись в один размер с ярлыком, никаких теней.
 * Четвёртой плиткой в дорожке ей не место: дорожка держит три, потому что на
 * 358px четвёртая сжимает остальные, а спокойный режим — не место, куда идут,
 * а способ смотреть на тот же список. */
.focuscta {
  justify-self: end;
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s3);
  border-radius: var(--r-pill);
  background: var(--paper-deep); color: var(--ink-muted);
  font-family: var(--font-body); font-weight: 700;
  font-size: var(--fs-caption); line-height: 1;
  text-decoration: none;
}
.focuscta:active { transform: scale(.96); }

/* ------------------------------ 30.2 экран ------------------------------- */

.shell--focus { padding-bottom: var(--s6); }

/* Герой стоит над делом, а не в шапке: здесь он не герб, а тот, кто ждёт
 * результат. Имя под героем — текст, а не кнопка: второй нажимаемой вещи рядом
 * с единственной целью на экране быть не должно. */
.focushero {
  display: grid; justify-items: center; gap: var(--s1);
  padding: var(--s5) 0 var(--s2);
}
.focushero .hero { --hero-size: 112px; }
.focushero__name {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-label); color: var(--ink-muted); line-height: 1.2;
}

/* Одна карточка, и она ровная: наклон — знак «это одна из многих», а здесь
 * дело одно, и чередовать его не с чем. Специфичность — нарочно та же, что у
 * правил наклона выше, и стоит ниже: иначе карточка одного дела висела бы
 * наискось, и спокойный экран начинался бы с ерзания. */
.kidsection.kidsection--focus .kidtask { transform: none; }

/* ------------------------------- 30.3 таймер ------------------------------ */

/* Пять минут без цифр: нечитающий ребёнок читает полоску, а не «4:59».
 * Кнопка во всю ширину, но тише карточки — без тени-стикера и без цвета
 * дела: это не вторая главная кнопка, а способ побыть рядом с делом. */
.focusclock {
  display: flex; align-items: center; gap: var(--s3);
  width: 100%; min-height: 52px;
  margin-top: var(--s3); padding: var(--s2) var(--s4);
  border-radius: var(--r-pill);
  background: var(--surface); box-shadow: var(--sh-1);
  color: var(--ink-muted);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-label); line-height: 1;
}
.focusclock__label { flex: 0 0 auto; }
.focusclock__track {
  position: relative; flex: 1 1 auto; height: 12px;
  border-radius: var(--r-pill); background: var(--paper-deep);
  overflow: hidden;
}
.focusclock__fill {
  display: block; width: 100%; height: 100%;
  border-radius: inherit; background: var(--sky);
  transform-origin: left center;
}
/* Ползёт от кода (inline transform), а не анимацией: правила тишины гасят
 * анимации целиком, и полоска осталась бы полной, пока голос уже сказал
 * «время вышло». Время здесь — информация. */
.focusclock.is-running { color: var(--ink); box-shadow: var(--sh-2); }
.focusclock.is-done { color: var(--green); }
.focusclock.is-done .focusclock__fill { background: var(--green-d); }

/* --------------------------- 30.4 тумблер тишины -------------------------- */

/* Один значок, а не пара: у выключателя звука их два, потому что «звука нет»
 * обязано быть видно даже в тишине, а у тишины выключенное состояние — просто
 * обычный экран. Включён — зелёная бумага и рамка: видно, но не кричит. */
.calmtoggle {
  flex: 0 0 auto;
  width: 44px; height: 44px; border-radius: var(--r-pill);
  display: grid; place-items: center; font-size: 20px; line-height: 1;
  background: rgba(255, 255, 255, .55); color: var(--ink);
  border: 2px solid var(--surface);
  box-shadow: var(--sh-sticker);
  transition: transform var(--dur-fast) var(--ease-spring), background-color var(--dur-fast) var(--ease-out);
}
.calmtoggle:active { transform: scale(.92); }
.calmtoggle__icon { display: block; line-height: 1; }
.calmtoggle.is-on {
  background: var(--green-t); color: var(--green); border-color: var(--green-d);
}

/* --------------------------- 30.5 дел больше нет -------------------------- */

/* Праздник без занавеса: герой в позе «ура», одна кнопка, которая говорит это
 * вслух, и выход. Зелёный здесь — цвет «всё хорошо», а не награда: дело уже
 * зачтено или его не было, и ничего начислять экран не собирается. */
.focusdone {
  position: relative;
  display: flex; align-items: center; gap: var(--s3);
  width: 100%; min-height: 76px;
  padding: var(--s4);
  border: 2px solid var(--green-d); border-radius: var(--r-lg);
  background: var(--green-t);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-body); color: var(--ink); text-align: left;
}
.focusdone__emoji { font-size: var(--fs-title); }
.focusdone__text { flex: 1 1 auto; min-width: 0; }
.focusdone__go { font-size: 22px; color: var(--green); }
.focusdone.is-speaking { border-color: var(--green); }
.focusdone__back { margin-top: var(--s3); }

/* ------------------- 31. узкий детский телефон (320–380) ------------------ */
/* Блок живёт в конце файла: правила выше по тексту не должны его перебивать,
 * как перебивал media-блок в середине, вставленный раньше .namecta. */

/* Три плитки в ряд на 320px ещё помещаются, а заголовку приглашения ширины
 * уже нет: он переносил «Назови героя» на две строки и делал карточку выше
 * первого задания. Тип меньше на шаг, ярлык тоже — смысл несёт голос. */
@media (max-width: 380px) {
  .namecta { gap: 0; }
  .namecta__emoji { font-size: var(--fs-title); }
  .namecta__title { font-size: var(--fs-body); }
  .namecta__body { padding-right: 40px; }
  .kiddest__figure { width: 64px; height: 64px; font-size: 30px; }
  .kiddest__figure .hero { --hero-size: 52px; }
  /* Спокойный экран (M6): герой на 112px съедает фол на 320-м, а тумблер
   * тишины в шапке отнимает у заголовка ширину, которой и так нет. */
  .focushero { padding-top: var(--s3); }
  .focushero .hero { --hero-size: 92px; }
  .focusclock { min-height: 48px; font-size: var(--fs-caption); }
  /* Подзаголовок группы на родительском экране прилипает к панели, которая
   * на 320px выше: кнопки в ней переносятся на вторую строку. */
  .taskgroup__title { top: 114px; }
}

/* ------------------- 32. шелл переживает каскад (≥640) -------------------- */
/* Модификаторы шелла — один класс, как и сам .shell, поэтому специфичность у
 * них равная и решает порядок в файле: .shell--kid стоит на 131-й строке, а
 * адаптивные ширины .shell — на 1464-й и 1472-й. Ниже по тексту — значит
 * сильнее: на планшете и десктопе детская колонка молча становилась 880px
 * вместо обещанных 560, а .shell--narrow (вход, PIN, правка дела) — вместо
 * 480. Играм повезло случайно: их правило стоит ниже адаптивного и потому
 * пережило каскад — его сохраняем явно.
 *
 * Блок в конце файла не «ещё одно правило», а починка: детская колонка не
 * расширяется никогда — ребёнку 2–8 лет «Готово!» работает, пока оно больше
 * руки, и каждая добавленная ширина только отодвигает цель. */
@media (min-width: 640px) {
  .shell--kid { max-width: 560px; }
  .shell--kid.shell--game,
  .shell--kid.shell--games { max-width: 640px; }
  .shell--narrow { max-width: 480px; }
}

/* ==========================================================================
   33. подписка (обновление 6)
   ==========================================================================
   Правила в этом блоке НЕ должны переигрывать то, что выше: здесь только
   новые классы и один модификатор существующего. Никаких правок выше по
   файлу — раздел 32 существует именно потому, что вставка в середину
   переворачивала каскад. */

/* ---- 33.1 платный вид на детском экране ---------------------------------- */
/* Платный вид НЕ прячется: ребёнок видит, что герой существует, и слышит его
 * примету. Приглушённая рамка и подпись вместо ссылки — разница между «нельзя»
 * и «это для тех, кто подписался». Цен здесь нет и быть не может. */
.speccard--locked { border-style: dashed; }
.speccard--locked .speccard__pick { color: var(--ink-muted); }
.speccard__sub {
  min-height: 32px; padding: 0 var(--s3);
  display: grid; place-items: center; width: 100%;
  border-radius: var(--r-pill);
  background: var(--paper-deep);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-caption); color: var(--ink-muted);
  text-decoration: none;
}
.speccard__sub:focus-visible { outline: 3px solid var(--ring); outline-offset: 2px; }

/* Отказ сервера. Тот же блок, что вопрос экрана, поэтому наследует .species-ask:
 * child уже умеет его рисовать и слушать. Отменён только нажим — это ответ, а
 * не приглашение. */
.species-ask--sub { border-style: solid; }
.species-ask--sub,
.species-ask--sub:active { cursor: default; transform: none; }

/* ---- 33.2 платный вид у родителя ---------------------------------------- */
/* Та же плитка, но ссылкой на объяснение, а не радиокнопкой: мёртвая кнопка
 * выглядела бы как поломка, а не как граница. */
.pick--locked { text-decoration: none; }
.pick--locked .pick__face { box-shadow: inset 0 0 0 2px var(--line); opacity: .75; }
.pick__mark {
  display: block; margin-top: var(--s1); padding: 2px 10px;
  justify-self: center;
  border-radius: var(--r-pill); background: var(--paper-deep);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-caption); color: var(--ink-muted);
  white-space: nowrap;
}
.pick--locked:focus-visible { outline: 3px solid var(--ring); outline-offset: 3px; }

/* ---- 33.3 раздел «Подписка» на дашборде --------------------------------- */
/* Спокойный блок без таймеров, бейджей «осталось» и красного: подписка — это
 * обещание семье, а не витрина. Поэтому бумага, а не акцент. */
.subcard {
  padding: var(--s4); border: 2px solid var(--line); border-radius: var(--r-lg);
  background: var(--paper);
  display: grid; gap: var(--s2); justify-items: start;
}
.subcard__tier {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-title); color: var(--ink);
}
.subcard__note { font-size: var(--fs-body); color: var(--ink-muted); }
.subcard__list,
.subcard__promise {
  margin: var(--s1) 0 0; padding: 0 0 0 var(--s4);
  font-size: var(--fs-body);
}
.subcard__promise { color: var(--ink-muted); }
.subcard__list li,
.subcard__promise li { margin-top: 4px; }
.subcard__hint { font-size: var(--fs-caption); color: var(--ink-muted); }
/* Ссылка внутри тихой подсказки карточки подписки — на страницу приватности:
 * спокойная, но отличимая от текста, иначе её не найдёт тот, кто ищет. */
.subcard__hint a { font-weight: 700; color: var(--ink); }
.subcard__confirm { display: grid; gap: var(--s2); justify-items: start; width: 100%; }
.subcard__ask { font-size: var(--fs-body); }
.subcard__row { display: flex; flex-wrap: wrap; gap: var(--s2); }

/* ---- 33.4 REF1: приглашение семьи ---------------------------------------- */
/* Ссылка с кодом — единственный элемент блока, который пересылают: она видна
 * целиком и переносится посимвольно, потому что обрезанный «…register?ref=»
 * переслать нельзя, а полный адрес длиннее колонки. Остальное — бумага и
 * тишина, как и вся карточка: приглашение — не витрина. */
.subcard__invite {
  display: grid; gap: var(--s2); justify-items: start; width: 100%;
  padding-top: var(--s2);
}
.subcard__invite-title {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-body); color: var(--ink);
}
.subcard__reflink {
  font-size: var(--fs-caption); color: var(--ink);
  word-break: break-all; max-width: 100%; text-align: left;
}
.subcard__invite-count { font-size: var(--fs-body); font-weight: 700; }

/* ---- 33.5 GIFT1: подарочная подписка ------------------------------------- */
/* Дарение и активация — редкие действия, поэтому оба блока свёрнуты:
 * заголовок сложен тише текста карточки (калька с pinchange__summary),
 * развораченный — получает обычную плотность subcard__invite. Формы внутри
 *details держат свою сетку и кнопку друг под другом: на 358px радиокнопки
 * срока в ряд — это три коротких слова, они помещаются и без переноса. */
.subcard__fold { width: 100%; }
.subcard__fold-summary {
  font-size: var(--fs-label); color: var(--ink-muted);
  cursor: pointer; justify-self: start; list-style: none;
}
.subcard__fold-summary::-webkit-details-marker { display: none; }
.subcard__fold[open] .subcard__fold-summary { color: var(--ink); }
.subcard__gift, .subcard__giftform {
  display: grid; gap: var(--s2); justify-items: start; width: 100%;
  padding-top: var(--s2);
}
.subcard__lengths {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s3);
  border: 0; padding: 0; margin: 0; width: 100%;
}
.subcard__length {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-body); color: var(--ink);
}
.subcard__length:has(input:disabled) { color: var(--ink-muted); }

/* ---------------- 34. шаг 3 ПК: тост по центру с 640px ------------------- */
/* На телефоне тост-лента на всю ширину — это размах под большой палец. На
 * мониторе тот же «left: s4; right: s4» растягивает «Задание зачтено» через
 * всё окно: сообщение на три слова и два метра экрана. С 640px тост сжимается
 * до подписи и встаёт по центру — туда, где кончается колонка, на которую
 * родитель смотрит. Центрирование через left: 50% + transform, а не margin:
 * auto — вход тоста анимирует transform, и keyframes внутри media-блока
 * переопределяет базовый только пока блок активен: ниже 640 движение то же,
 * что было. */
@media (min-width: 640px) {
  .toast {
    left: 50%; right: auto; transform: translateX(-50%);
    width: max-content; max-width: 420px;
  }
  @keyframes toast-in {
    from { opacity: 0; transform: translateX(-50%) translateY(16px); }
  }
}

/* ------------- 35. шаг 4 ПК: верхняя панель по ширине колонки ------------- */
/* .topbar — sticky во всю ширину окна, и на 2560px монитор это бренд у левого
 * края и «Выйти» у правого, в двух метрах друг от друга. Панель не сужаем —
 * иначе клейкая подложка и линия под ней оборвутся раньше окна, — а
 * выравниваем содержимое с колонкой: проценты в паддинге считаются от ширины
 * самой панели, поэтому бренд встаёт ровно над шеллом. Цель — 1040px до 1280
 * и 1200px после: ширины шага 5 (.shell--wide), поэтому панель сегодня чуть
 * шире контента — панель, прыгающая между страницами, хуже панели чуть шире
 * контента. max() держит телефонный --page-pad ниже порога. */
@media (min-width: 900px) {
  .topbar {
    padding-left:  max(var(--page-pad), calc((100% - 1040px) / 2));
    padding-right: max(var(--page-pad), calc((100% - 1040px) / 2));
  }
}
@media (min-width: 1280px) {
  .topbar {
    padding-left:  max(var(--page-pad), calc((100% - 1200px) / 2));
    padding-right: max(var(--page-pad), calc((100% - 1200px) / 2));
  }
}

/* ==========================================================================
   LEGAL1 (обновление 9): согласие родителя на регистрации.

   Нативный чекбокс с accent-color из палитры: галочка согласия должна
   выглядеть и работать как родная на любом устройстве — кастомный
   переключатель был бы украшением на пути к юридическому факту. Карточка
   вокруг — та же «наклейка», что у полей ввода, только кликабельна
   вся карточка: промахнуться по галочке согласия должно быть так же трудно,
   как по кнопке. Ошибка согласия подсвечивает рамку карточки, а не только
   текст вверху формы: родитель должен увидеть, где именно не хватает
   отметки, а не искать её глазами по всей форме.
   ========================================================================== */

.field--consent .consent {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s3);
  align-items: start;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
}
.field--consent .consent:hover { border-color: var(--amber); }
.field--consent .consent input {
  width: 22px;
  height: 22px;
  margin-top: 2px;
  accent-color: var(--green);
}
.field--consent .consent__text {
  font-size: var(--fs-body);
  line-height: 1.55;
  color: var(--ink);
}
.field--consent .consent__text a { color: var(--ink); font-weight: 700; }
.field--consent.field--error .consent {
  border-color: var(--coral);
  background: var(--coral-t);
}
.field--consent.field--error .consent input { accent-color: var(--coral-p); }

/* ==========================================================================
   36. D5: детское кольцо — низкая навигация, комната-дом, шкаф, магазин
   Эталон design/DESIGN-KIDS-UI.md: вход = комната, 4 дороги внизу, шкаф и
   магазин отдельными экранами, экран заданий — медаль + имя ребёнка.
   ========================================================================== */

/* --------------------------- 36.1 нижняя навигация ------------------------ */

/* Фиксированная полоса: большой палец ребёнка живёт внизу экрана, и четыре
 * дороги кольца стоят ровно там. Полоса — бумага на волоске, не стекло и не
 * тень на пол-экрана: комната за ней остаётся комнатой. */
.kidnav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; flex-direction: column; gap: var(--s1);
  max-width: 560px; margin: 0 auto;
  padding: var(--s2) var(--s2) calc(var(--s2) + env(safe-area-inset-bottom, 0px));
  background: var(--paper);
  border-top: 2px solid var(--line);
}
.kidnav__tabs {
  display: flex; justify-content: center; gap: var(--s2);
}
/* Тихий вход в кабинет (SEC1): взрослый находит его взглядом, ребёнок —
 * нет. Та же тишина, что у прежней ссылки в подвале детских экранов. */
.kidnav__parent {
  align-self: center;
  font-size: var(--fs-caption); line-height: 1;
  font-weight: 700;
  color: var(--ink-faint);
  text-decoration: none;
  padding: var(--s1) var(--s3);
  border-radius: var(--r-sm);
}
.kidnav__parent:active { color: var(--ink-muted); }
.kidnav__tab {
  flex: 1 1 0;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: var(--s1) 0;
  border-radius: var(--r-sm);
  color: var(--ink-muted);
  text-decoration: none;
}
.kidnav__emoji {
  display: grid; place-items: center;
  width: 48px; height: 48px;
  border-radius: 50%;
  background: var(--paper-deep);
  font-size: 24px; line-height: 1;
  transition: transform var(--dur-pop) var(--ease-spring), background var(--dur-fast) linear;
}
.kidnav__label {
  font-size: var(--fs-caption); line-height: 1.2;
  font-weight: 700;
}
.kidnav__tab--on { color: var(--ink); }
.kidnav__tab--on .kidnav__emoji {
  background: var(--amber);
  box-shadow: inset 0 -3px 0 var(--amber-p);
  transform: translateY(-2px);
}
.kidnav__tab:active .kidnav__emoji { transform: scale(.92); }

/* Полоса фиксирована — содержимое обязано её пережить: последняя тихая полоса
 * дома и «Другой герой» больше не прячутся под ней. */
.shell--kid { padding-bottom: calc(var(--s7) + 128px); }

@media (min-width: 640px) {
  .kidnav {
    border: 2px solid var(--line);
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    bottom: 0;
    box-shadow: var(--sh-1);
  }
}
@media (prefers-reduced-motion: reduce) {
  .kidnav__emoji { transition: none; }
  .kidnav__tab--on .kidnav__emoji { transform: none; }
}

/* ------------------------- 36.2 шапка экрана заданий ---------------------- */

/* Эталон, решение 7: медаль + имя РЕБЁНКА + голос, и сразу карточки. Медаль —
 * стадия героя в кольце цвета «сделано»: тот же значок, что в занавесе
 * награды, а не новый символ. */
.taskhead {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s4);
}
.taskhead__medal {
  display: grid; place-items: center;
  width: 56px; height: 56px; flex: 0 0 auto;
  border-radius: 50%;
  background: var(--green-t);
  box-shadow: inset 0 0 0 3px var(--green);
  font-size: 28px; line-height: 1;
}
.taskhead__name {
  /* min-width: 0, а не min-content (урок gamehead, цикл 50): min-content
   * держал имя на самом длинном слове, и при «Константин» дисплеем ряд
   * выталкивал тумблер звука за край экрана. Длинное имя переносится —
   * шапка становится выше, а не шире. */
  flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-display); line-height: 1.1;
}
.taskhead .voice-toggle { flex: 0 0 auto; }

/* --------------------------- 36.3 комната-дом ----------------------------- */

/* У дома нет заголовка: сцена и есть заголовок. Кошелёк и уровень — слева,
 * где глаз ребёнка начинает обход; голос — справа, где он и на остальных
 * экранах. */
.roomhead--home {
  flex-wrap: nowrap;
  margin-top: var(--s4);
}
.roomhead--home .purse--home {
  padding: var(--s2) var(--s3);
  border-radius: var(--r-pill);
  background: var(--amber-t);
  font-size: var(--fs-body);
}
.roomhead--home .badge--level { flex: 0 0 auto; }
.roomhead--home .voice-toggle { margin-left: auto; }

/* Пузырь — реплика героя «Привет!»: слово в нём то же, что произносит голос.
 * Живёт над героем, хвостиком к нему; тихий, потому что герой здоровается,
 * а не объявляет. */
.roomstage__bubble {
  position: absolute; left: 58%; bottom: 66%;
  max-width: 34%;
  padding: var(--s2) var(--s3);
  background: var(--surface);
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-body); line-height: 1.15; color: var(--ink);
}
.roomstage__bubble::after {
  content: "";
  position: absolute; left: 14px; bottom: -9px;
  width: 14px; height: 14px;
  background: var(--surface);
  border-left: 2px solid var(--line);
  border-bottom: 2px solid var(--line);
  transform: rotate(-45deg);
}

/* Точка касания шкафа (B5): сам шкаф нарисован в сцене (ROOM_BASE) и стоит
 * на плинтусе, а якорь — невидимый прямоугольник ровно по нему: x 2.5…37.5
 * из 240, y 70…128 из 180. Проценты совпадают с viewBox, потому что фигура
 * и сцена — один и тот же квадрат. Прежняя дверца висела на стене поверх
 * окна и читалась кривой — теперь разъехаться картинки и кнопки не могут. */
.roomstage__wardrobe {
  position: absolute; left: 1%; top: 38.9%;
  width: 14.6%; height: 32.2%;
  border-radius: var(--r-sm);
  text-decoration: none;
}
.roomstage__wardrobe:active { background: rgba(255, 255, 255, .22); }
/* Ручка шкафа — круглый значок на дверцах, брат карандаша в углу сцены:
 * тот же круг, та же рамка, тот же кивок нажатия. Слово здесь не поместится
 * (шкаф у края сцены), поэтому значок говорит голосом, как вся сцена. */
.roomstage__knob {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  display: grid; place-items: center;
  width: 40px; height: 40px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--line);
  font-size: 20px; line-height: 1;
}
.roomstage__wardrobe:active .roomstage__knob {
  transform: translate(-50%, -50%) scale(.92);
}

/* Карандаш «Изменить комнату» — правый верхний угол сцены: единственная
 * честная дорога к изменению комнаты — мебель, и карандаш ведёт в магазин
 * на вкладку «Мебель». Кружок, как все тихие кнопки экрана. */
.roomstage__edit {
  position: absolute; right: var(--s2); top: var(--s2);
  display: grid; place-items: center;
  width: 44px; height: 44px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--line);
  font-size: 20px; line-height: 1;
  text-decoration: none;
  transition: transform var(--dur-pop) var(--ease-spring);
}
.roomstage__edit:active { transform: scale(.92); }

/* Тихая смена героя на общем планшете: одна строка по центру, видна тому,
 * кто ищет, невидна тому, кто не знает о втором герое. */
.roomhero-switch {
  display: block;
  width: fit-content; margin: var(--s5) auto 0;
  padding: var(--s2) var(--s4);
  border-radius: var(--r-pill);
  background: var(--paper-deep);
  font-size: var(--fs-caption); font-weight: 700;
  color: var(--ink-muted); text-decoration: none;
}

/* ---------------------------- 36.4 шкаф героя ----------------------------- */

/* Примерочная: герой в сегодняшнем наряде на мягкой бумаге — «как я выгляжу»
 * стоит до списка вещей, потому что переодевание начинается с этого вопроса. */
.wardfit {
  display: grid; justify-items: center; gap: var(--s2);
  margin: var(--s4) 0 var(--s5);
  padding: var(--s5) var(--s4);
  background: var(--paper-deep);
  border-radius: var(--r-xl);
}
.wardfit .hero { --hero-size: 150px; }

.wardsec { margin-top: var(--s5); }
.wardsec__title {
  font-size: var(--fs-label); font-weight: 800; letter-spacing: .02em;
  color: var(--ink-muted);
  margin-bottom: var(--s3);
}

/* Сетка вещей та же, что в магазине: шкаф и магазин — один мир, и вещь в них
 * узнаётся по одной и той же карточке. */
.wardgrid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: var(--s3);
}
.wardcard {
  display: flex; flex-direction: column; align-items: center; gap: var(--s2);
  padding: var(--s3) var(--s2);
  background: var(--surface);
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-1);
}
/* Надетое узнаётся без чтения: янтарное кольцо и галочка, тот же жест, что у
 * выбранного вида героя и купленной вещи. */
.wardcard--on { border-color: var(--amber); box-shadow: var(--sh-1), inset 0 0 0 3px var(--amber-t); }
.wardcard__art svg { width: 64px; height: 64px; display: block; }
.wardcard__name {
  font-size: var(--fs-caption); font-weight: 700; line-height: 1.2;
  text-align: center;
}
.wardcard__btn {
  min-height: 44px; padding: var(--s1) var(--s4);
  border: none; border-radius: var(--r-pill);
  background: var(--amber);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-caption); color: var(--on-accent);
  cursor: pointer;
}
.wardcard--on .wardcard__btn { background: var(--paper-deep); color: var(--ink-muted); }
.wardcard.is-worn { animation: pop var(--dur-pop) var(--ease-spring); }

/* Пустой шкаф честен: рамка с корзиной вместо выдуманных вещей. */
.wardempty {
  display: grid; justify-items: center; gap: var(--s1);
  padding: var(--s6) var(--s4);
  border: 2px dashed var(--line);
  border-radius: var(--r-lg);
  color: var(--ink-muted);
}
.wardempty__emoji { font-size: 40px; line-height: 1; }
.wardempty__title {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-body); color: var(--ink);
}
.wardempty__hint { font-size: var(--fs-caption); text-align: center; }

/* Вид героя и магазин — две тихие дороги внизу шкафа: то и другое тоже
 * «переодеть», но другими руками. */
.wardrow {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s3);
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  text-decoration: none;
}
.wardrow__emoji { font-size: 26px; line-height: 1; }
.wardrow__title {
  flex: 1 1 auto;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-body); color: var(--ink);
}
.wardrow__go {
  font-size: var(--fs-caption); font-weight: 700;
  color: var(--ink-muted);
}
.wardrow--shop { box-shadow: var(--sh-1); }

/* ----------------------------- 36.5 магазин ------------------------------- */

/* Три полки — пилюли с эмодзи НАД подписью, как у нижней навигации: нечитающий
 * ребёнок различает полку фигурой и положением. Эмодзи и текст в строку не
 * помещаются даже на 358 (замер: ряду нужно 357px при 310 доступных — третья
 * пилюля уезжала за край экрана), в столбец — влезают и на 320. Активная —
 * янтарная. */
.shoptabs {
  display: flex; gap: var(--s2);
  margin: var(--s4) 0 var(--s4);
  /* Сезоны (дизайн-лок 2026-09-05) сделали полок пять, и на 320px одна
   * строка больше не вмещает ни одну подпись без обрезки («Игрушки» — одно
   * слово, не переносится). Прятать подпись нельзя — её читает родитель,
   * помогающий ребёнку; нечитающий и так идёт по эмодзи. */
  flex-wrap: wrap;
}
.shoptabs__tab {
  flex: 1 1 calc(20% - var(--s2));
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px;
  min-height: 64px;
  padding: var(--s1) var(--s1);
  border-radius: var(--r-pill);
  background: var(--paper-deep);
  border: 2px solid transparent;
  text-decoration: none;
  font-size: var(--fs-caption); font-weight: 700;
  color: var(--ink-muted);
}
.shoptabs__tab--on {
  background: var(--amber-t);
  border-color: var(--amber);
  color: var(--ink);
}
.shoptabs__emoji { font-size: 20px; line-height: 1; }
.shoptabs__label { line-height: 1.2; white-space: nowrap; }

/* Узкий экран: перенос решаем мы, а не шрифт. Пять неподдающихся подписей
 * целиком не входят вплоть до ~430px, но на глазах это выглядит не «не
 * входят», а «вошли четыре, пятая растянулась бревном на всю строку» —
 * flex-grow одинокой вкладке даёт ширину banners. Ровные три плюс две
 * ниже 430, одна строка из пяти выше — обе картинки читаются как задуманные
 * (замер цикла «Сезоны»: 4+1 с растянутой до 342px «Сезонами» на 390). */
@media (max-width: 429px) {
  .shoptabs__tab { flex-basis: calc(33.333% - var(--s2)); }
}

/* «Уже куплено» — строки, а не карточки: купленное не продаётся, и кнопки
 * «купить» под ним быть не может. Галочка, вещь, имя и честное состояние. */
.shopown-sec { margin-top: var(--s6); }
.shopown-sec__title {
  font-size: var(--fs-label); font-weight: 800; letter-spacing: .02em;
  color: var(--ink-muted);
  margin-bottom: var(--s3);
}
.shopown {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s3);
  border-radius: var(--r-md);
  text-decoration: none;
}
.shopown + .shopown { border-top: 2px solid var(--line); }
.shopown__check {
  display: grid; place-items: center;
  width: 28px; height: 28px; flex: 0 0 auto;
  border-radius: 50%;
  background: var(--green);
  /* Белый на зелёном — токеном --surface, как у всех галочек магазина:
     raw hex в этой части файла запрещает тест «ни одного цвета мимо токенов». */
  color: var(--surface); font-size: 15px; font-weight: 800;
}
.shopown__art svg { width: 44px; height: 44px; display: block; }
.shopown__name {
  flex: 1 1 auto;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-body); color: var(--ink);
}
.shopown__state {
  font-size: var(--fs-caption); font-weight: 700;
  color: var(--ink-muted);
  /* Состояние не теснится и не режется: на широком экране оно справа, как
   * всегда, а на узком (галочка + вещь + имя + «Куплено · стоит» = шире
   * строки) переезжает на свою строку вправо — замер цикла «Сезоны» на 320
   * показал, что одна строка тут не собирается никогда. */
  flex: 0 0 auto;
  margin-left: auto;
}
/* Свежая покупка: строка вспыхивает янтарным и подпрыгивает — «что именно
 * теперь моё» говорит она, потому что карточка в сетке уже исчезла. */
.shopown.is-bought {
  background: var(--amber-t);
  animation: pop var(--dur-pop) var(--ease-spring);
}

@media (prefers-reduced-motion: reduce) {
  .wardcard.is-worn { animation: none; }
  .shopown.is-bought { animation: none; }
}

/* --------------------------- 36.6 широкие экраны -------------------------- */

@media (min-width: 640px) {
  .wardgrid { grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); }
  .wardfit .hero { --hero-size: 190px; }
  .roomstage__bubble { font-size: var(--fs-title); }
}

/* ==========================================================================
   37. D5 проход 2: родительский кабинет — нижняя навигация, колокольчик,
   модалка подтверждений, нижний лист выдачи, вкладки страницы ребёнка
   Эталон design/DESIGN-PARENT-UI.md + docs/ui/d5-pass1-audit-parent.md.
   Хром — контурные иконки (.icon, lucide-геометрия из спрайта): линии
   наследуют currentColor и перекрашиваются вместе с состоянием кнопки.
   ========================================================================== */

/* ------------------------- 37.0 контурные иконки -------------------------- */

.icon {
  width: 24px; height: 24px; flex: none;
  fill: none; stroke: currentColor;
  stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round;
}
.icon--s { width: 18px; height: 18px; }
.btn--sm .icon { width: 18px; height: 18px; }

/* ----------------------------- 37.1 зелёное ------------------------------- */

/* «Зачесть» — единственное действие продукта, которое говорит «да, сделано»:
 * зелёный, как галочка зачёта на детском экране. Текст — surface (белый),
 * как у всех зелёных состояний приложения. */
.btn--green { background: var(--green); color: var(--surface); box-shadow: var(--sh-2); }
.btn--green:hover { background: var(--green-h); box-shadow: var(--sh-3); }
.btn--green:active { background: var(--green-p); }
.btn--green .icon { stroke-width: 3; }

/* ------------------------ 37.2 шапка трёх вкладок ------------------------- */

.topbar--parent .topbar__actions { gap: var(--s3); }

/* Колокольчик — тихий, пока тихо: контур без бейджа не кричит. Бейдж — coral:
 * число видно краем глаза, и именно оно, а не звон, зовёт родителя. */
.pbell, .pexit {
  position: relative;
  display: grid; place-items: center;
  width: 44px; height: 44px; flex: none;
  border-radius: 50%;
  color: var(--ink); text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out);
}
.pbell:hover, .pexit:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); }
.pbell .icon { width: 22px; height: 22px; stroke-width: 2.4; }
.pbell__badge {
  position: absolute; top: -2px; right: -2px;
  min-width: 20px; height: 20px; padding: 0 var(--s1);
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--coral); color: var(--ink);
  font-size: 11px; font-weight: 800; line-height: 1;
  border: 2px solid var(--paper);
}
/* Ноль ждёт молча: место под бейдж занято, чтобы колокольчик не прыгал,
 * когда число приедет. */
.pbell__badge--zero { visibility: hidden; }

/* Страница ребёнка: назад — круглая тихая кнопка слева, как у эталона. */
.topbar__back {
  display: grid; place-items: center;
  width: 40px; height: 40px; flex: none;
  border-radius: 50%;
  background: var(--paper-deep); color: var(--ink); text-decoration: none;
}
.topbar__back:active { background: var(--line); }

/* ------------------------- 37.3 нижняя навигация -------------------------- */

/* Взрослая сестра .kidnav: те же три дороги всегда, но спокойнее — disc 40px
 * вместо 48, контур вместо эмодзи, surface вместо бумаги. Родитель находит
 * дорогу по тому, что снизу не меняется. */
.pnav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; justify-content: center; gap: var(--s4);
  max-width: 640px; margin: 0 auto;
  padding: var(--s2) var(--s2) calc(var(--s2) + env(safe-area-inset-bottom, 0px));
  background: color-mix(in srgb, var(--surface) 95%, transparent);
  backdrop-filter: blur(6px);
  border-top: 2px solid var(--line);
}
.pnav__tab {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  width: 104px; padding: var(--s1) 0;
  color: var(--ink-muted); text-decoration: none;
}
.pnav__disc {
  display: grid; place-items: center;
  width: 40px; height: 40px;
  border-radius: 50%;
  background: var(--paper);
  transition: background var(--dur-fast) var(--ease-out), transform var(--dur-pop) var(--ease-spring);
}
.pnav__disc .icon { width: 20px; height: 20px; stroke-width: 2.4; }
.pnav__label { font-size: var(--fs-caption); font-weight: 700; line-height: 1.2; text-align: center; }
.pnav__tab--on { color: var(--ink); }
.pnav__tab--on .pnav__disc {
  background: var(--amber);
  box-shadow: inset 0 -3px 0 var(--amber-p);
  transform: translateY(-2px);
}
.pnav__tab:active .pnav__disc { transform: scale(.92); }

/* Полоса фиксирована — содержимое обязано её пережить. */
.shell--pnav { padding-bottom: calc(var(--s7) + 116px); }

@media (min-width: 640px) {
  .pnav {
    border: 2px solid var(--line);
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    box-shadow: var(--sh-1);
  }
}
@media (prefers-reduced-motion: reduce) {
  .pnav__disc { transition: none; }
  .pnav__tab--on .pnav__disc { transform: none; }
}

/* ---------------------- 37.4 карточки детей («Семья») ---------------------- */

/* Медаль, а не плитка стадии: цвет — идентификация ребёнка, вид героя родитель
 * увидит на его странице. Угол карточки тронут цветом ребёнка — тот же цвет,
 * что у медали, карточки семьи различаются с первого взгляда. */
/* D5-2: на телефоне карточка одна в ряду — двум нет места из-за кнопки
 * «Задание» и счётчика дел; min(100%, 240px) не дасть треку сжаться под
 * содержимое и вывалиться за экран. На широких — снова два-три в ряду. */
.kidgrid { display: grid; gap: var(--s4); grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
.kidgrid .kidcard { min-width: 0; }
.kidcard {
  position: relative; overflow: hidden;
  display: grid; gap: var(--s4);
  padding: var(--s4);
  background: var(--surface);
  border: 2px solid color-mix(in srgb, var(--line) 50%, transparent);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-sticker);
}
.kidcard::before {
  content: ''; position: absolute; top: -44px; right: -44px;
  width: 132px; height: 132px;
  border-radius: 0 0 0 100%;
  background: var(--paper-deep);
}
.kidcard--green::before { background: var(--green-t); }
.kidcard--coral::before { background: var(--coral-t); }
.kidcard--sky::before { background: var(--sky-t); }
.kidcard--berry::before { background: var(--berry-t); }
.kidcard--sun::before { background: var(--amber-t); }

.kidcard__head {
  position: relative;
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3);
  color: inherit; text-decoration: none;
}
.kidcard__medal {
  display: grid; place-items: center; flex: none;
  width: 64px; height: 64px;
  border-radius: 50%;
  background: var(--surface);
  font-size: 32px; line-height: 1;
  box-shadow: inset 0 0 0 4px var(--line);
  transition: transform var(--dur-pop) var(--ease-spring);
}
.kidcard--green .kidcard__medal { box-shadow: inset 0 0 0 4px var(--green); }
.kidcard--coral .kidcard__medal { box-shadow: inset 0 0 0 4px var(--coral); }
.kidcard--sky .kidcard__medal { box-shadow: inset 0 0 0 4px var(--sky); }
.kidcard--berry .kidcard__medal { box-shadow: inset 0 0 0 4px var(--berry); }
.kidcard--sun .kidcard__medal { box-shadow: inset 0 0 0 4px var(--amber); }
.kidcard__head:hover .kidcard__medal { transform: scale(1.05); }
.kidcard__names { display: grid; gap: 2px; padding-top: var(--s1); min-width: 0; }
.kidcard__name { font-family: var(--font-display); font-weight: 700; font-size: 26px; line-height: 1.05; }
.kidcard__hero { font-size: var(--fs-label); font-weight: 700; color: var(--ink-muted); }
.kidcard__foot {
  position: relative;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid color-mix(in srgb, var(--line) 50%, transparent);
}
.kidcard__meta { font-size: var(--fs-caption); font-weight: 700; color: var(--ink-muted); }
.kidcard__alert { color: var(--berry); white-space: nowrap; }
.kidcard__alert .icon { width: 14px; height: 14px; vertical-align: -.2em; }
.kidcard__add {
  display: inline-flex; align-items: center; gap: var(--s1);
  padding: var(--s2) var(--s4);
  min-height: 44px; box-sizing: border-box;
  border-radius: 16px;
  background: var(--paper-deep);
  color: var(--ink); text-decoration: none;
  font-weight: 700; font-size: 15px;
  transition: background var(--dur-fast) var(--ease-out);
}
.kidcard__add:hover { background: var(--line); }
.kidcard__add .icon { width: 18px; height: 18px; stroke-width: 2.6; }

/* -------------------- 37.5 модалка «Ждут подтверждения» -------------------- */

/* <details> без open не существует на экране — ни скрима, ни места под него.
 * Открытие — атрибутом из адреса (?confirm=1), закрытие — тапом по скриму
 * (он же summary), без сценария. */
.pmodal:not([open]) { display: none; }
.pmodal { position: fixed; inset: 0; z-index: var(--z-modal); }
.pmodal__scrim, .tsheet__scrim {
  position: fixed; inset: 0;
  display: block;
  background: var(--scrim);
  backdrop-filter: blur(2px);
  list-style: none; cursor: default;
}
.pmodal__scrim::-webkit-details-marker,
.tsheet__scrim::-webkit-details-marker { display: none; }

.pmodal__card {
  position: absolute; left: var(--s4); right: var(--s4); top: 50%;
  transform: translateY(-50%);
  max-height: 82dvh; overflow-y: auto;
  display: grid; gap: var(--s4);
  padding: var(--s5);
  background: var(--surface);
  border: 2px solid color-mix(in srgb, var(--line) 50%, transparent);
  border-radius: var(--r-xl);
  box-shadow: var(--sh-sticker);
}
.pmodal__head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }
.pmodal__title { font-size: var(--fs-title); line-height: 1.1; }
/* Число очереди — цветом «ягоды» (тревога кабинета), не кораллом:
 * коралл на белом не держит контраста мелкому числу. */
.pmodal__n { color: var(--berry); }
.pmodal__close {
  display: grid; place-items: center;
  width: 44px; height: 44px; flex: none;
  border-radius: 50%;
  background: var(--paper); color: var(--ink-muted); text-decoration: none;
}
.pmodal__close:active { background: var(--paper-deep); }
.pmodal__list { display: grid; gap: var(--s3); }
.pmodal__hint { text-align: center; font-size: var(--fs-caption); font-weight: 700; color: var(--ink-faint); }

/* Карточка внутри модалки: бумажная, а не белая — очередь решений лежит НА
 * столе, а не наоборот. Медаль маленькая (44px): здесь она опознает ребёнка,
 * а не представляет его. */
.pcard {
  display: grid; gap: var(--s3);
  padding: var(--s4);
  background: var(--paper);
  border: 2px solid color-mix(in srgb, var(--line) 50%, transparent);
  border-radius: var(--r-lg);
}
.pcard__head { display: flex; align-items: center; gap: var(--s3); }
.pcard__medal {
  display: grid; place-items: center; flex: none;
  width: 44px; height: 44px;
  border-radius: 50%;
  font-size: 22px; line-height: 1;
}
.pcard--green .pcard__medal { background: var(--green-t); box-shadow: inset 0 0 0 3px var(--green); }
.pcard--coral .pcard__medal { background: var(--coral-t); box-shadow: inset 0 0 0 3px var(--coral); }
.pcard--sky .pcard__medal { background: var(--sky-t); box-shadow: inset 0 0 0 3px var(--sky); }
.pcard--berry .pcard__medal { background: var(--berry-t); box-shadow: inset 0 0 0 3px var(--berry); }
.pcard--sun .pcard__medal { background: var(--amber-t); box-shadow: inset 0 0 0 3px var(--amber); }
.pcard__body { flex: 1 1 auto; min-width: 0; }
.pcard__kid {
  font-size: var(--fs-caption); font-weight: 800;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-muted);
}
.pcard__title { font-family: var(--font-display); font-weight: 700; font-size: 18px; line-height: 1.2; }
.pcard__title strong { white-space: nowrap; }
.pcard__time { font-size: 11px; font-weight: 700; color: var(--ink-muted); white-space: nowrap; }
.pcard__actions { display: flex; align-items: center; gap: var(--s2); }
.pcard__act { flex: 1 1 0; display: flex; }
.pcard__act .btn { width: 100%; }
/* «Вернуть» — бумажная, а не белая с контуром: она реже нужна и не должна
 * спорить с «Зачесть» за первый взгляд. */
.pcard__act .btn--quiet { background: var(--paper-deep); color: var(--ink-muted); box-shadow: none; }
.pcard__act .btn--quiet:hover { color: var(--ink); }
/* --------------------- 37.6 нижний лист «Дать задание» -------------------- */

.tsheet:not([open]) { display: none; }
.tsheet { position: fixed; inset: 0; z-index: var(--z-modal); }
.tsheet__body {
  position: absolute; left: 0; right: 0; bottom: 0;
  max-height: 92dvh; overflow-y: auto;
  background: var(--surface);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  box-shadow: var(--sh-sheet);
  padding: 0 var(--s5) calc(var(--s5) + env(safe-area-inset-bottom, 0px));
}
.tsheet__grab {
  width: 48px; height: 6px;
  margin: var(--s3) auto var(--s1);
  border-radius: var(--r-pill);
  background: var(--line);
}
.tsheet__head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: var(--s2) 0 var(--s3);
}
.tsheet__title { font-size: 28px; line-height: 1.1; }
.tsheet__close {
  display: grid; place-items: center;
  width: 48px; height: 48px; flex: none;
  border-radius: 50%;
  background: var(--paper); color: var(--ink); text-decoration: none;
}
.tsheet__close:active { background: var(--line); }
.tsheet__form { gap: var(--s5); }
.tsheet__submit { width: 100%; font-size: var(--fs-title); }
.tsheet__full { padding: var(--s2) 0 var(--s4); font-size: var(--fs-body); color: var(--ink-muted); }
/* В листе шаблоны кладутся строками, а не лентой: место есть, лист не экономит. */
.tmpl-row--wrap { flex-wrap: wrap; overflow-x: visible; padding-bottom: 0; }

/* -------------------- 37.7 сегментные пикеры листа ------------------------ */

/* Контейнер — углубление (бумага + внутренняя тень), выбранная кнопка —
 * «вынута» из него (surface + тень). Выбор виден ДО отправки формы, и это
 * не скрипт: :has(input:checked) переставляет подсветку сам, по нативному
 * радиотапу. Серверная пометка --on рисует то же до всякого CSS-рессора. */
.seg {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: var(--s1);
  padding: 6px;
  background: var(--paper);
  border: 2px solid color-mix(in srgb, var(--line) 50%, transparent);
  border-radius: var(--r-lg);
}
.seg label {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  min-height: 48px;
  border-radius: var(--r-md);
  color: var(--ink-muted);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.seg input { position: absolute; opacity: 0; inset: 0; pointer-events: none; }
.seg label:has(input:checked),
.seg__kid--on, .seg__opt--on {
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--sh-1);
}
.seg label:has(input:focus-visible) { outline: 3px solid var(--ring); outline-offset: 2px; }
.seg--kids label { gap: var(--s2); padding: var(--s2) var(--s3); }
.seg__kid-emoji { font-size: 24px; line-height: 1; }
.seg__kid-name { font-family: var(--font-display); font-weight: 700; font-size: 18px; }
.seg__kid-count { font-size: var(--fs-caption); font-weight: 800; color: var(--ink-faint); }
.seg__kid-count--full { color: var(--berry); }
.seg__opt-face { display: grid; justify-items: center; gap: 2px; padding: var(--s2) var(--s3); }
.seg__opt-name { font-family: var(--font-display); font-weight: 700; font-size: 17px; line-height: 1.1; }
.seg__opt-range { font-size: var(--fs-caption); font-weight: 800; color: var(--ink-muted); }
.seg label:has(input:checked) .seg__opt-range { color: var(--ink); }

/* ------------------------ 37.8 вкладки страницы ребёнка -------------------- */

.ptabs {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s1);
  margin-top: var(--s4);
  padding: 4px;
  background: var(--paper-deep);
  border: 1px solid color-mix(in srgb, var(--line) 50%, transparent);
  border-radius: var(--r-md);
}
.ptabs__tab {
  display: flex; align-items: center; justify-content: center;
  min-height: 44px;
  border-radius: var(--r-sm);
  font-weight: 700; font-size: 15px;
  color: var(--ink-muted); text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.ptabs__tab--on { background: var(--surface); color: var(--ink); box-shadow: var(--sh-1); }
.ptabs__tab:active { background: color-mix(in srgb, var(--ink) 6%, transparent); }

/* Шапка ребёнка в кабинете: медаль в цвете, имя, имя героя и одна строка
 * «сколько осталось». XP-бар уехал в «Прогресс» — шапка называет ребёнка,
 * а не отчитывается о нём. */
.pchildhead { display: flex; align-items: center; gap: var(--s4); margin-top: var(--s4); }
.pchildhead__medal {
  display: grid; place-items: center; flex: none;
  width: 64px; height: 64px;
  border-radius: 50%;
  background: var(--surface);
  font-size: 32px; line-height: 1;
  box-shadow: inset 0 0 0 4px var(--line);
}
.pchildhead--green .pchildhead__medal { background: var(--green-t); box-shadow: inset 0 0 0 4px var(--green); }
.pchildhead--coral .pchildhead__medal { background: var(--coral-t); box-shadow: inset 0 0 0 4px var(--coral); }
.pchildhead--sky .pchildhead__medal { background: var(--sky-t); box-shadow: inset 0 0 0 4px var(--sky); }
.pchildhead--berry .pchildhead__medal { background: var(--berry-t); box-shadow: inset 0 0 0 4px var(--berry); }
.pchildhead--sun .pchildhead__medal { background: var(--amber-t); box-shadow: inset 0 0 0 4px var(--amber); }
.pchildhead__main { min-width: 0; }
.pchildhead__name {
  font-family: var(--font-display); font-weight: 700; font-size: 24px;
  line-height: 1.1; text-wrap: balance;
}
.pchildhead__hero { font-family: var(--font-body); font-size: var(--fs-label); font-weight: 700; color: var(--ink-muted); margin-left: var(--s2); }
.pchildhead__meta {
  margin-top: 2px;
  font-size: 11px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-muted);
}
.pchildhead__games { margin-top: 2px; font-size: var(--fs-caption); font-weight: 700; color: var(--ink-muted); }

/* Пунктирная строка «Дать задание» в конце списка дел: приглашение, а не
 * кнопка-команда — открывает лист. */
.givetask {
  display: flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 56px;
  margin-top: var(--s3);
  border: 2px dashed var(--line);
  border-radius: var(--r-lg);
  color: var(--ink); text-decoration: none;
  font-family: var(--font-display); font-weight: 700;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.givetask:hover { border-color: var(--ink-muted); }
.givetask .icon { width: 20px; height: 20px; stroke-width: 2.6; }

/* --------------------- 37.9 шаблоны и настройки вкладок -------------------- */

.tplgrid { display: grid; gap: var(--s4); }
.tplcard {
  display: flex; align-items: center; gap: var(--s4);
  padding: var(--s4);
  background: var(--surface);
  border: 2px solid color-mix(in srgb, var(--line) 50%, transparent);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-sticker);
  text-decoration: none; color: inherit;
}
.tplcard__emoji {
  display: grid; place-items: center; flex: none;
  width: 56px; height: 56px;
  border-radius: 50%;
  background: var(--paper);
  box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--amber) 70%, transparent);
  font-size: 28px; line-height: 1;
}
.tplcard__body { flex: 1 1 auto; min-width: 0; display: grid; gap: 2px; }
.tplcard__title { font-family: var(--font-display); font-weight: 700; font-size: 18px; line-height: 1.2; }
.tplcard__hint {
  font-size: var(--fs-caption); font-weight: 700; color: var(--ink-faint);
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.tplcard__tags { display: flex; gap: var(--s2); margin-top: 2px; }
.tplcard__tag {
  font-size: 11px; font-weight: 800;
  padding: 2px var(--s2);
  border-radius: var(--r-pill);
}
.tplcard__tag--daily { color: var(--green-p); background: var(--green-t); }
.tplcard__tag--once { color: var(--berry-p); background: var(--berry-t); }
.tplcard__tag--xp { color: var(--ink-muted); background: var(--amber-t); }
.tplcard__give {
  display: grid; place-items: center; flex: none;
  width: 44px; height: 44px;
  border-radius: 50%;
  background: var(--amber); color: var(--ink);
  box-shadow: var(--sh-1);
  transition: transform var(--dur-pop) var(--ease-spring), background var(--dur-fast) var(--ease-out);
}
.tplcard__give:hover { background: var(--amber-h); }
.tplcard__give:active { transform: scale(.92); }
.tplcard__give .icon { width: 22px; height: 22px; stroke-width: 3; }

/* «Своё задание» — пунктирная карта: не нашли нужного — придумайте. */
.tplcard--own {
  background: transparent;
  border: 2px dashed var(--line);
  box-shadow: none;
}
.tplcard--own:hover { border-color: var(--ink-muted); }
.tplcard--own .tplcard__emoji { background: var(--amber-t); box-shadow: none; }
.tplcard__own-body { flex: 1 1 auto; display: grid; gap: 2px; }
.tplcard__own-btn {
  justify-self: start; margin-top: 2px;
  font-weight: 800; font-size: var(--fs-label);
  color: var(--ink-muted);
}

/* Настройки — две группы с общим заголовком, подписка отдельно и тише. */
.pgroup { margin-top: var(--s5); display: grid; gap: var(--s4); }
.pgroup__hint { font-size: var(--fs-caption); font-weight: 700; color: var(--ink-faint); }
.pquit { margin-top: var(--s6); display: grid; }
.pquit__btn {
  justify-self: center;
  min-height: 48px; padding: var(--s2) var(--s5);
  border-radius: var(--r-pill);
  color: var(--ink-muted); font-weight: 700;
  transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.pquit__btn:hover { color: var(--ink); background: var(--paper-deep); }

@media (min-width: 640px) {
  .tplgrid { grid-template-columns: 1fr; }
  .pmodal__card { left: 15%; right: 15%; }
}
@media (prefers-reduced-motion: reduce) {
  .kidcard__head:hover .kidcard__medal { transform: none; }
  .tplcard__give { transition: none; }
}

/* ==========================================================================
   38. TG1 — панель «Уведомления» в настройках.
   Панель — те же «наклейки», что ПИН и голос: ставится один раз. Код
   связи длиннее семейного втрое (24 знака + дефисы), поэтому у него своя
   гарнитура: мельче, теснее, перенос по дефисам — две строки на 320px
   это норма, а не ошибка (дизайн-лок §1). Галочка имени — карточка-
   «наклейка» как согласие на регистрации: кликабельна вся, промахнуться
   по юридическому факту должно быть трудно.
   TG2 — путь в один тап: кнопка deep link'а и QR той же ссылки.
   ========================================================================== */

.pairing__code--tg {
  font-size: calc(var(--fs-title) * 1.05);
  letter-spacing: .08em;
  line-height: 1.5;
  overflow-wrap: break-word;
}

/* TG2: путь в один тап. Тело карточки — одна сетка с полем, как у форм
 * (.panel form): до этого дети панели стояли вплотную к краям и друг к
 * другу — код выпускают редко, и дефект дожил до кнопки, которая сделала
 * его заметным. Кнопки — своего размера, не на всю ширину: одна мысль,
 * одна тап-цель; QR — по центру, ссылка и картинка говорят одно и то же. */
.tgcode { display: grid; gap: var(--s3); padding: var(--s4); }
.tgcode > .btn { justify-self: start; }
.tgqr { display: grid; justify-items: center; gap: var(--s2); }
.tgqr__plate svg {
  display: block;
  width: 132px;
  height: 132px;
  border-radius: var(--r-md);
  box-shadow: inset 0 0 0 2px var(--line);
}
.tgqr figcaption { text-align: center; }

.tgform { display: grid; gap: var(--s3); }
.tgcheck {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s3);
  align-items: start;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
}
.tgcheck:hover { border-color: var(--amber); }
.tgcheck input {
  width: 22px;
  height: 22px;
  margin-top: 2px;
  accent-color: var(--green);
}
.tgcheck__text { font-size: var(--fs-body); line-height: 1.55; color: var(--ink); }

/* ==========================================================================
   38.1 AUTH1 Ф2 — карточка «Телефон и почта».
   Панель повторяет анатомию соседних — ПИН, Телеграм (те же .panel,
   .pairing, .field), новых визуальных слов нет. Единственное своё —
   каналы, которых ещё нет: они стоят в общем переключателе .seg, но
   приглушены и без курсора. Выбор, за которым ничего не стоит, обязан
   выглядеть как обещание, а не как сломанная кнопка.
   ========================================================================== */
.seg__opt--off { cursor: default; opacity: .5; }
.seg__opt--off .seg__opt-range { color: var(--ink-faint); }

/* Выгрузка журнала (TG1-rider): последняя строка секции «Журнал», после
   пейджера. Ссылка, а не форма — у GET нет побочных эффектов; подпись
   тихая, потому что родителю важно «откроется без танцев», а не «CSV». */
.historyexport {
  margin-top: var(--s4);
  display: grid;
  gap: var(--s2);
  justify-items: start;
}

/* ==========================================================================
   39. GAMES3 — «Повтори за мной»: поле Simon (дизайн-лок цикла 41, §2.3).
   Подсветка — СОСТОЯНИЕ, а не анимация (§5.7): .is-lit держится ≥700 мс
   и меняет фон с рамкой мгновенно, без keyframes и transition. Спокойный
   режим (M6) и prefers-reduced-motion давят длительности до 0,01 мс —
   анимационная вспышка сделала бы серию невидимой именно для того, кого
   этот режим бережёт. Единственный transition здесь — масштаб при нажатии:
   украшение, которое в тишине исчезает без потерь. Звери — виды героя
   продукта, поэтому у каждой кнопки свой мягкий чип; подсветка у всех
   одна — янтарная: «горит» должно означать одно и то же, а не три цвета.
   ========================================================================== */

.simonfield {
  display: grid;
  gap: var(--s3);
  max-width: 440px;
  margin-inline: auto;
}
.simon {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--s4);
  min-height: 76px;
  padding: var(--s3) var(--s4);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 4px solid var(--paper-deep);
  box-shadow: var(--sh-sticker);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--fs-title);
  color: var(--ink);
  text-align: left;
  touch-action: manipulation;
  transition: transform var(--dur-fast) var(--ease-spring);
}
.simon:active { transform: scale(.97); }
.simon__emoji {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: var(--r-md);
  font-size: calc(var(--fs-display) * .9);
  line-height: 1;
}
.simon:nth-child(1) .simon__emoji { background: var(--coral-t); }
.simon:nth-child(2) .simon__emoji { background: var(--sky-t); }
.simon:nth-child(3) .simon__emoji { background: var(--berry-t); }

/* Состояние «загорелся»: тот же мгновенный класс, что держат таймеры игры.
   Чип зверя светлеет, чтобы эмодзи читался и на янтарном. */
.simon.is-lit {
  background: var(--amber);
  border-color: var(--amber-p);
  box-shadow: var(--sh-2);
}
.simon.is-lit .simon__emoji { background: var(--amber-h); }

/* Промах — на секунду тише, без встряски: раскачивание клетки, на которую
   нельзя сердиться, выглядело бы выговором. Прозрачность — мгновенная,
   анимации нет и в обычном режиме. */
.simon--wrong { opacity: .45; }

/* Палец героя (пол после двух промахов): статичное кольцо, как у
   .choice.is-invited в спокойном режиме — жест обязан читаться и там,
   где пульс придавлен до нуля. */
.simon.is-invited { box-shadow: var(--sh-sticker), 0 0 0 6px var(--amber-p); }

/* Пока серия показывается, кнопки не отвечают: тап по кнопке во время
   показа — это ещё не ответ, и нажимать «сквозь» серию нельзя. */
.simonfield.is-showing .simon { pointer-events: none; }

@media (min-width: 640px) {
  .simonfield { gap: var(--s4); }
  .simon { min-height: 84px; }
}

/* ------ цикл 46, якорь агента B: «Кто новенький?» (память, ур. 7) ------ */
/* Агент B наполняет ТОЛЬКО блок ниже: ряд гостей, затемнение на подмене
   (opacity, не transform — спокойный режим), варианты-кнопки. */
/* «Кто новенький?»: ряд гостей. Пятеро в линию не влезают в кегль missing
   (memitem 1.2×display с межбуквенным шагом gamecountline) — кегль ряда
   масштабируется числом гостей, как ступени лесенки (цикл 45):
   контейнер-запрос мерит фактическую ширину, --guests ставит клиент; и
   4, и 5 мест дают клетку крупнее пальца. Урок второго прогона гейта-46:
   контейнер НЕ мерит сам себя — cqi в свойствах самого .guestrow
   fallback-ится к вьюпорту (358/320), и пятёрка переполняла ряд на
   16/18px; контейнер обязан быть ПРЕДКОМ, потому кегль меряет
   .guestwrap. Делитель 1.3 против фактического схода глифа 1.25em даёт
   ~4% запаса. Затемнение подмены — ТОЛЬКО opacity: никаких
   трансформаций, спокойный режим и prefers-reduced-motion давят
   переход до 0,01 мс, но конечное состояние (гаснет) остаётся — гость
   подменяется в темноте при любых настройках. Проявление — тот же
   переход обратно, клиент снимает класс после отрисовки. */
.guestwrap { container-type: inline-size; }
.guestrow {
  display: flex; justify-content: center; flex-wrap: nowrap;
  gap: var(--s2); width: 100%;
  font-size: min(calc(var(--fs-display) * 1.25),
    calc((100cqi - (var(--guests, 4) - 1) * var(--s2)) / var(--guests, 4) / 1.3));
  line-height: 1.15;
  transition: opacity var(--dur-base) var(--ease-out);
}
/* memitem несёт свой кегль 1.2×display (missing) — в ряду гостей кегль
   задаёт сам ряд, чтобы обе фазы стояли одинаково и место подмены не
   выдавало себя размером. */
.guestrow .memitem { font-size: 1em; }
.guestrow.is-away { opacity: 0; }

/* ------ цикл 46, якорь агента A: «Что будет потом?» (логика, ур. 8) ----- */
/* Сцена-причина: ряд больших картинок, стрелка времени и пунктирная
   прорубь «что дальше» — рамка «здесь будет» (прецедент дырки лесенки
   и memhole; знак вопроса — для родителя через плечо, не для чтения).
   Кегль сцены — как у опорного ряда отличий (1.05×display): картинки
   больше кнопок не бывают, ответ — герой раунда. flex-wrap, а не
   контейнер-запрос: два-три глифа и так влезают, перенос строчки
   спокойнее сжатия на 320. */
.nextscene {
  display: flex; align-items: center; justify-content: center;
  gap: var(--s3); flex-wrap: wrap; width: 100%;
}
.nextscene .bigemoji { font-size: calc(var(--fs-display) * 1.05); }
.nextarrow {
  font-family: var(--font-display); font-weight: 800;
  font-size: calc(var(--fs-title) * 1.2); color: var(--ink-faint);
  line-height: 1;
}
.nextslot {
  display: grid; place-items: center;
  min-width: 56px; min-height: 56px;
  border: 3px dashed var(--amber-p); border-radius: var(--r-md);
  color: var(--amber-p);
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-title); line-height: 1;
}
@media (min-width: 640px) {
  .nextslot { min-width: 64px; min-height: 64px; }
}

/* ------ цикл 48, «Разложи оттенки» (цвета и формы, 4–6, ур. 4) ------ */
/* Четыре плашки в ряд вместо трёх: grading сравнивает СВЕТЛОТУ, и все
   четыре стимула обязаны стоять перед глазами одновременно — сетка 2×2
   прятала бы сравнение за переводом взгляда. Плашка — тот же стикер,
   что swatch «Волшебных красок», но растянута на кнопку: изоляция
   стимула означает одинаковый размер, форму и радиус, меняется только
   заливка (--sw несёт hsl из клиента). Кнопка ниже обычной (96, не
   116): кегль плашки и так ограничен шириной колонки, а на 320px
   четверка с высокими кнопками выталкивала бы вопрос за первый экран. */
.choices--shades { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s2); }
.choices--shades .choice { min-height: 96px; padding: var(--s2); }
.shadeplate {
  display: block; width: 100%; aspect-ratio: 1;
  border-radius: var(--r-md);
  background: var(--sw, var(--paper-deep));
  box-shadow: inset 0 0 0 3px rgba(43, 33, 24, .08), var(--sh-1);
}
@media (min-width: 640px) {
  .choices--shades .choice { min-height: 128px; }
}

/* ------ цикл 48, «Собери картинку из фигур» (цвета и формы, 5–8) ------ */
/* Сцена-картинка — один инлайновый SVG на всю ширину доски (прецедент
   «Найди фигуру»): проценты viewBox масштабируют сцену без запросов и
   без картинок; max-width держит картинку «крупной, не гигантской» на
   планшете — фигуры-кандидаты обязаны оставаться сопоставимы с ней.
   Пунктир дырки рисуется в самом SVG (атрибуты элемента): рамка
   повторяет форму пропавшей фигуры и является подсказкой-контуром. */
.mosscene {
  display: block; width: 100%; max-width: 320px; height: auto;
  margin: 0 auto;
  filter: drop-shadow(0 2px 6px rgba(43, 33, 24, .10));
}
@media (min-width: 640px) {
  .mosscene { max-width: 380px; }
}

/* ------ цикл 49, «Такой же предмет» (цвета и формы, 4–6, ур. 4) ------ */
/* Образец крупнее кандидатов (104 против 80): он цель сличения, а не один
   из равных — разница кеглей и говорит «сравнивай с ним», не словами. */
.samelead__shape { width: 104px; height: 104px; }
@media (min-width: 640px) {
  .samelead__shape { width: 128px; height: 128px; }
}

/* ------ цикл 49, «Столько же» (счёт, 4–6, ур. 5) ------ */
/* Хозяин образца меньше и отодвинут: он история («у зайки»), а не счётный
   материал — считать надо морковки, и кегль говорит это без слов. */
.sameowner {
  font-style: normal; font-size: .72em; line-height: 1;
  opacity: .9;
}
/* Образец «столько же»: хозяин над предметами, предметы одной строкой.
 * Замер живой партией (probe-samecount-worst.mjs, цикл 53): грид здесь
 * МЁРТВ — пять предметов это один текстовый узел, то есть один анонимный
 * грид-итем в первой колонке (вычисленные колонки «220px 0 0 0 0»), и
 * repeat(3/5, auto) не переносит ничего: ряд — одна неразрывная строка.
 * Сегодня она влезает: 228.4px при 320 (запас 54px), 244.1px при 375
 * (запас 9px). Если кегль образца вырастет, ряд ВЫЛЕЗЕТ, а не перенесётся;
 * разнести предметы по отдельным элементам — правка рендера (очередь
 * цикла 54), не комментария. Прецедент переполнения — гейт 49 (52px). */
.gamecountline--stack {
  display: flex; flex-direction: column; align-items: center;
  letter-spacing: normal;
}
.countrow {
  display: inline-grid; grid-template-columns: repeat(3, auto);
  gap: 0 .06em; justify-items: center; line-height: 1.05;
}
/* Тарелка кандидата — полный ряд до пяти предметов (240px при 320):
   три колонки .choices давали кнопке ~90px, тарелка шире — гейт 49
   поймал переполнение 52px, а мельчить нельзя: считать надо предметы.
   Кандидаты потому стоят в один столбец (прецедент полнорождных рядов
   .moreplate, кегль почти display). */
.choices--counts { grid-template-columns: minmax(0, 1fr); }
.countplate {
  display: flex; flex-wrap: wrap; gap: 0 .06em; justify-content: center;
  font-size: calc(var(--fs-display) * .95); line-height: 1.05;
}

/* ------ цикл 51, «Что было сначала?» (память, 6–8, ур. 8) ------ */
/* Сцена запоминания: звери стоят в ряду с первого кадра и занимают своё
   место сразу — появляется только сам зверь (прозрачность, не display:
   ряд не прыгает, и место в нём не выдаёт ответ до своего имени). Кегль
   и межбуквенный шаг наследует от gamecountline/memitem, как у всей
   памяти. Переход давят глобальные правила calm и prefers-reduced-motion —
   конечное состояние то же, приходит в миг. */
.firstsitem {
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.firstsitem.is-on { opacity: 1; }

/* -------- SE-класс: кадры высотой до 600px (гейт review-se-frame) -------- */

/* Кадр 320×568 (SE-класс) до цикла 52 не покрывал никто: ревью-гейты
 * мерят 358 и 320×640, а контент раундов впритык. На коротком кадре
 * четыре игры вываливали раунд за фолд — ребёнок листал экран посреди
 * партии. Правило кадра: раунд целиком на фолде, ответ не уезжает за
 * кромку (D2: одна ясная вещь за раз). Блок действует ТОЛЬКО ниже 600px
 * высоты — вёрстка 358/320×640 и все пороги игр не тронуты; мельчить
 * ровно настолько, насколько нужно, и не ниже пола тапа. */
@media (max-height: 600px) {
  /* Общее для раундов: кнопка «послушать» садится на пол тапа (было 56),
   * ряды и отступы между блоками раунда — на шаг ниже. */
  .sayword { width: 44px; height: 44px; font-size: 22px; }
  .gamelead { margin-bottom: var(--s3); }
  .choices { gap: var(--s2); }

  /* «Найди пару»: двенадцать карточек тремя колонками — четыре ряда;
   * четыре колонки дают три ряда, карточки остаются выше пола тапа. */
  .memgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s2); }
  .memcard { min-height: 0; }

  /* «Собери картинку из фигур»: сцена 320px шириной стояла 240px высотой. */
  .mosscene { max-width: 216px; }
  .shape { width: 64px; height: 64px; }

  /* «Первая буква»: слово-подсказка и ответы — на кегль ниже. Тарелка
   * слова едет за колонкой (13.2cqi), а не за кадром: самое длинное слово
   * пула «ЧЕРЕПАХА» («…ЕРЕПАХА», 8 знаков) при кегле display×1 = 40px
   * занимает 281px (253 текста + 28 паддинга) и в колонке 272px (кадр 320)
   * переносилось на вторую строку — доска росла 290 → 350px, низ 615 при
   * фолде 568, ребёнок листал посреди партии (дефект из «Попутной находки»
   * цикла 66, зонд probe-letters-long.mjs). 13.2cqi даёт 35.9px на 272 и
   * тарелку 255px — запас 17px; на 358 (колонка 310) clamp упирается в
   * максимум display×1 и кадр остаётся ровно как был. Мельче некуда: кегль
   * ниже 35px уже коридор ≤520 (display×.85), а тарелка — подсказка к
   * картинке, которую ребёнок ещё и читает. Паттерн тот же, что у
   * .gameword--read выше: cqi меряет .gameword (контейнер — предок,
   * не сама тарелка). */
  .gameword__emoji { font-size: calc(var(--fs-display) * 1.35); }
  .gameword__text { font-size: clamp(22px, 13.2cqi, calc(var(--fs-display) * 1)); padding: 2px 14px; }
  .bigletter { font-size: calc(var(--fs-display) * 1.2); }

  /* Цикл 58, «Собери слово»: буквы — таблички одной буквы, а не карточки-
   * картинки: ряд 72 против 100 (тот держит max-width: 380) возвращает
   * худший раунд (5 кнопок, два ряда) на фолд 568. Замер живой партией
   * (гейт 58-го, кадры 358/320×568): было 605/611 — ребёнок листал
   * посреди партии; стало 550/544. Пол тапа 44 — с запасом. */
  .choices--letters { gap: var(--s2); }
  .choices--letters .choice { min-height: 72px; padding: var(--s2); }

  /* «Столько же» — замер живой партией (probe-samecount-worst.mjs,
   * цикл 53: 45 тарелок из пяти предметов на трёх кадрах, ноль дефектов).
   * Арифметику здесь НЕ выводить из кегля: шаг эмодзи 1.272em (Noto Color
   * Emoji), а не 1.2em калибровки-49. И КАКИЕ значения применяются —
   * зависит от кадра: на 320 и 358 действует узкий блок ниже (≤359),
   * кегль тарелки .56 = 22.4px и min-height 72, а НЕ эти .62/84; они
   * применяются только на 375×568. Перенос тарелки из пяти предметов:
   * 3+2 при 320 (тарелка 109px против content 109.5), 4+1 при 358
   * (127.9 против 128.4 — горизонтальный запас полпикселя, самый тонкий
   * в системе) и 4+1 при 375 (136.6 против 137.2). «Один ряд» недостижим
   * ни на одном из трёх кадров: нужно 146–161px content, столбец даёт
   * 109–137. Двухрядная тарелка в кнопке: 49.3px против content 50.7
   * при 320/358 — запас 1.4px, впритык; при 375 — 54.9 против 63.3.
   * Раунд целиком на фолде — теперь ИЗМЕРЕНИЕ, не утверждение: низ
   * последней кнопки 554.5 из 568 при 320, 531.9 при 358. */
  .gamecountline { font-size: calc(var(--fs-display) * .95); }
  .countrow { grid-template-columns: repeat(5, auto); }
  .choices--counts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .choices--counts .choice { min-height: 84px; padding: var(--s2); }
  .countplate { font-size: calc(var(--fs-display) * .62); }
}

/* Самый тесный кадр (уже 360 по ширине — селектор накрывает и кадр 358,
 * которым меряет гейт; по высоте — всё, что ниже 600): «Столько же» —
 * единственный раунд, которому не хватило четырнадцати пикселей.
 * Предметы остаются считаемыми, тарелка — выше пола тапа. Замер цикла 53
 * (probe-samecount-worst.mjs): на 358 тарелка пяти предметов ложится
 * в 4+1 при запасе полпикселя по горизонтали (127.9 против 128.4 content)
 * и 1.4px по вертикали (49.3 против 50.7) — держится, но дальше сжимать
 * некуда: любое решение здесь только живым замером. */
@media (max-height: 600px) and (max-width: 359px) {
  .gamecountline { font-size: calc(var(--fs-display) * .88); }
  .choices--counts .choice { min-height: 72px; }
  .countplate { font-size: calc(var(--fs-display) * .56); }
}

/* Подиум движковых игр на самом узком кадре (≤339 — 320, но не 358):
 * 46vw при 320 даёт 147, и «Первому звуку» не хватило ОДНОГО пикселя до
 * фолда (гейт 58-го, живая партия: низ доски 569 при 568). 136px — подиум
 * остаётся сценой героя, низ доски 557. На 358 не действует: там сцена
 * 164 и фолд держался без этого правила. */
@media (max-height: 600px) and (max-width: 339px) {
  .gamestage { height: 136px; }
}

/* ------ Ступень ≤520: коридор 481…520 (гейт review-frame-ladder, цикл 56) ------ */
/* Пре-регистрация (docs/qa/2026-09-06-cycle56-geometry-plan.md §2.1): блок
   ≤600 калиброван на 568 и на 481 сжимает недостаточно — десять игр
   вываливали раунд за фолд (замер 55-го, кадр 320×481, низ доски):
   Первая буква 555 · Столько же 553 · Собери картинку +62 · Повтори за
   мной 529 · Такой же предмет 527 · Найди пару 505 · Найди цифру 505 ·
   Найди рифму 491 · Прочитай слово 491 · Лесенка 486.
   Блок стоит ПОСЛЕ узкого ≤359 сознательно: внутри коридора он обязан
   WIN у обоих предыдущих, а 568 в него не входит вовсе — ступени
   se-frame (358/320×568) не тронуты ни на пиксель. Промпт — на кегль
   body: на 28px короткая фраза переносилась на две строки, и разброс
   высоты подсказки между прогонами (42px: низ доски «Найди цифру» 505
   при той же доске 264px, что и 463) был лотереей семпла, а не
   геометрией. Пол тапа 44 держится всеми правилами ниже. */
@media (max-height: 520px) {
  /* Общее для всех игр: шапка, промпт, доска и ряды — на шаг компактнее. */
  .gamehead { margin-top: var(--s2); }
  .gameprompt { margin-top: var(--s2); font-size: var(--fs-body); }
  .gameboard { margin-top: var(--s2); }
  .gamelead { margin-bottom: var(--s2); }
  .choices { gap: var(--s2); }
  .choice { min-height: 84px; padding: var(--s2); }
  .bigemoji { font-size: calc(var(--fs-display) * 1.15); }
  .bigletter, .bignum { font-size: calc(var(--fs-display) * 1.15); }

  /* «Первая буква»: слово-подсказка на два кегля ниже, ответы — на один. */
  .gameword { gap: var(--s1); }
  .gameword__emoji { font-size: calc(var(--fs-display) * 1.4); }
  .gameword__text { font-size: calc(var(--fs-display) * .85); padding: 2px 12px; }

  /* «Найди пару»: двенадцать карточек — ТРИ ряда по 72px вместо четырёх
     по 60; карточка 62×72, выше пола тапа. */
  .memgrid { grid-auto-rows: 72px; gap: var(--s2); }
  .memcard { min-height: 0; aspect-ratio: auto; }

  /* «Повтори за мной»: три ряда по 60px, чип зверя 44 — в пол тапа. */
  .simonfield { gap: var(--s2); }
  .simon { min-height: 60px; padding: var(--s1) var(--s3); }
  .simon__emoji { width: 44px; height: 44px; font-size: calc(var(--fs-display) * .75); }

  /* «Собери картинку из фигур»: сцена 168px шириной (высота 126),
     фигуры-кандидаты 48 при кнопке 68. */
  .mosscene { max-width: 168px; }
  .shape { width: 52px; height: 52px; }
  .choices--scene .choice { min-height: 68px; }
  .choices--scene .shape { width: 48px; height: 48px; }

  /* «Такой же предмет»: образец 72 против кандидатов 48 — разница кеглей,
     которая говорит «сравнивай с ним», остаётся (было 104/56). */
  .samelead__shape { width: 72px; height: 72px; }

  /* «Столько же»: два ряда кандидатов по 60px — тарелка двух рядов
     предметов 2×22.4×1.05 ≈ 47px + паддинг 8 = 55, влезает; линия образца
     на кегль ниже, предметы остаются считаемыми. */
  .choices--counts .choice { min-height: 60px; padding: var(--s1); }
  .gamecountline { font-size: calc(var(--fs-display) * .8); }

  /* «Больше или меньше»: тарелка в половинной кнопке — кегль .95, чтобы
     2+2+1 зверят влезал в высоту при 481. */
  .moreplate { font-size: min(calc(var(--fs-display) * .95), 24cqi); }
  /* + потолок 24cqi из ветки верификатора: в коридоре число то же,
     что гейтил цикл 56 (при кнопке в пол-экрана min берёт .95),
     но при уже кнопке потолок не даёт вернуться переполнению. */
  .choices--diffrow .choice { min-height: 56px; }
}
/* ==========================================================================
 * §99 HERO2 — ЗАБОТА О ГЕРОЕ (Update 15). ВЕСЬ CSS заботы живёт здесь.
 * ==========================================================================
 * Границы блока для слияний: всё между этим баннером и «КОНЕЦ §99» — забота,
 * и только она (ветка cycle57-hero2). Ничего из существующего
 * файла блок не правит — только добавляет, и стоит последним по двум причинам,
 * и обе несущие:
 *
 *   1. Порядок = вес. Селекторы последовательностей НЕ сильнее поз (§25.4) и
 *      нрава вида (§25.4a): к каждому стоит [data-pose], и только поэтому
 *      `.hero[data-care="feed"][data-pose] .h-head` догоняет по весу
 *      `.hero[data-species="bunny"] [data-pose="idle"] .h-head`. Равный вес
 *      решается порядком — блок ниже обоих, и «ест» побеждает «слушает».
 *   2. Файл один и версионируется одним ASSET_VERSION: второй лист — второй
 *      запрос на телефоне и второй ключ кэша, а забота ставится на страницу,
 *      которая и так самая тяжёлая.
 *
 * Правила заботы:
 *   — забота — это ДЕЙСТВИЕ, а не реакция: ребёнок ткнул «Покормить», и герой
 *     ест — тянется к миске, наклоняется, жуёт, еда убывает. Никаких общих
 *     прыжков: у каждого акта своя последовательность;
 *   — последовательность = атрибут data-care на корне SVG, который ставит
 *     СЕРВЕР в миг заботы. Поэтому она играется и без JavaScript — герой
 *     всё равно ест, пьёт и засыпает, просто молча;
 *   — реквизит показывает data-prop (одна рука с §25.5), а свою жизнь —
 *     бег, убывание, каплю — выдаёт только последовательность;
 *   — всё конечное, кроме цикличных Zzz и поглаживаний: забота — сцена с
 *     началом и концом, а не новое бесконечное состояние;
 *   — забота никогда не наказывает: ни одного красного, ни одного «пусто»,
 *     ни одной строки долга. Сделанное — тише, а не хуже.
 * ========================================================================== */

/* ---- 99.1 реквизит заботы: миска, поилка, мяч ---------------------------- */
/* Миска и поилка стоят на земле перед героем — там же, где его тень, чтобы
 * читалось «перед ним», а не «в воздухе». Еда — три кружка внутри миски. */
.hero .p-ball-line { fill: none; stroke: var(--surface); stroke-width: 2.5; stroke-linecap: round; }
.hero .p-glint { fill: color-mix(in srgb, var(--surface) 82%, transparent); }
.hero .p-heart { fill: var(--coral); opacity: 0; }
.hero .p-zzz {
  fill: var(--ink); font-family: var(--font-display); font-weight: 800;
  font-size: 15px; text-anchor: middle; opacity: 0;
}
/* Всё, что последовательность двигает: fill-box даёт каждой части её собственный
 * центр, поэтому «по одному» получается без трёх рукописных origin'ов — и не
 * зависит от того, куда её потом передвинут в SVG. */
.hero .p-food ellipse, .hero .p-water, .hero .p-drop,
.hero .p-ball, .hero .p-zzz, .hero .p-heart {
  transform-box: fill-box; transform-origin: center;
}
/* Показ реквизита — тот же жест, что у всех предметов героя (§25.5): атрибут
 * data-prop и правило display. Новой руки показа здесь нет. */
.hero[data-prop="bowl"] [data-art="bowl"],
.hero[data-prop="cup"] [data-art="cup"],
.hero[data-prop="ball"] [data-art="ball"] {
  display: block;
  animation: prop-in var(--dur-pop) var(--ease-spring) backwards;
}

/* ---- 99.2 ПОКОРМИТЬ ------------------------------------------------------ */
/* Тянется к миске и наклоняется, рот работает, еда исчезает по одной —
 * «пустеет» видно без единого слова. */
.hero[data-care="feed"][data-pose] .h-arm--r { animation: care-scoop 860ms ease-in-out 6 alternate; }
.hero[data-care="feed"][data-pose] .h-head { animation: care-nod 860ms ease-in-out 6 alternate; }
.hero[data-care="feed"] .h-mouth--open { animation: care-chew 860ms ease-in-out 6; }
.hero[data-care="feed"] .p-food ellipse:nth-child(1) { animation: care-bite 320ms ease-in 600ms forwards; }
.hero[data-care="feed"] .p-food ellipse:nth-child(2) { animation: care-bite 320ms ease-in 1750ms forwards; }
.hero[data-care="feed"] .p-food ellipse:nth-child(3) { animation: care-bite 320ms ease-in 2900ms forwards; }

/* ---- 99.3 НАПОИТЬ -------------------------------------------------------- */
/* Ниже наклоняется к поилке, вода в ней уходит, капля падает. */
.hero[data-care="drink"][data-pose] .h-head { animation: care-sip 1150ms ease-in-out 4 alternate; }
.hero[data-care="drink"][data-pose] .h-arm--r { animation: care-scoop 1150ms ease-in-out 4 alternate; }
.hero[data-care="drink"] .p-water { animation: care-drain 4600ms ease-in forwards; }
.hero[data-care="drink"] .p-drop { animation: care-drip 1150ms ease-in 4; }

/* ---- 99.4 ПОИГРАТЬ ------------------------------------------------------- */
/* Прыгает и бьёт по мячу — мяч катится к ногам и возвращается. */
.hero[data-care="play"][data-pose] .h-root { animation: hero-jump 620ms var(--ease-spring) 8; }
.hero[data-care="play"][data-pose] .h-arm--l { animation: hero-arm-up-l 620ms var(--ease-spring) 8; }
.hero[data-care="play"][data-pose] .h-arm--r { animation: hero-arm-up-r 620ms var(--ease-spring) 8; }
.hero[data-care="play"] .h-mouth--open { opacity: 1; }
.hero[data-care="play"] .p-ball { animation: care-roll 2.6s ease-in-out 2 alternate; }

/* ---- 99.5 УЛОЖИТЬ СПАТЬ -------------------------------------------------- */
/* Сворачивается, глаза закрываются и НЕ мигают, одеяло дышит, буквы сна
 * поднимаются по очереди. */
.hero[data-care="tuck"][data-pose] .h-root { animation: care-settle 2.4s ease-out forwards; }
.hero[data-care="tuck"] .h-eye { animation: none; transform: scaleY(.12); }
.hero[data-care="tuck"][data-pose] .h-head { animation: hero-sleep-tilt 6.4s ease-in-out infinite; }
/* Дыхание сна — тот же такт, что в позе sleep, и вдвое медленнее бодрствования.
 * Тот же вес, что у парения дракончика: без него уснувший дракончик висел бы в
 * воздухе над одеялом. */
.hero[data-care="tuck"][data-pose] .h-body { animation: hero-breathe 6.4s ease-in-out infinite; }
.hero[data-care="tuck"][data-pose] .h-arm--l,
.hero[data-care="tuck"][data-pose] .h-arm--r { animation: none; }
.hero[data-care="tuck"] .h-mouth--open { opacity: .55; }
.hero[data-care="tuck"] .p-zzz--1 { animation: care-zzz 2.4s ease-in-out 600ms infinite; }
.hero[data-care="tuck"] .p-zzz--2 { animation: care-zzz 2.4s ease-in-out 1300ms infinite; }
.hero[data-care="tuck"] .p-zzz--3 { animation: care-zzz 2.4s ease-in-out 2s infinite; }

/* ---- 99.6 ПОГЛАДИТЬ ------------------------------------------------------ */
/* Голова жмётся к ладони, глаза поднимаются на руку — «следят» за ней, —
 * румянец теплеет, сердце выпрыгивает у щеки. */
.hero[data-care="stroke"][data-pose] .h-head { animation: care-nuzzle 1.8s ease-in-out infinite alternate; }
.hero[data-care="stroke"] .h-eye { animation: care-gaze 1.8s ease-in-out infinite alternate; }
.hero[data-care="stroke"] .h-cheek { animation: care-blush 1.8s ease-in-out infinite alternate; }
.hero[data-care="stroke"] .p-heart { animation: care-love 1.8s ease-in-out 2; }

/* ---- 99.7 ключевые кадры заботы ----------------------------------------- */
@keyframes care-scoop {
  from { transform: rotate(-8deg); }
  to { transform: rotate(-27deg); }
}
@keyframes care-nod {
  from { transform: translateY(0) rotate(0deg); }
  to { transform: translateY(2.5px) rotate(6deg); }
}
@keyframes care-chew {
  0%, 100% { opacity: 0; }
  50% { opacity: 1; }
}
@keyframes care-bite {
  to { transform: scale(0); opacity: 0; }
}
@keyframes care-sip {
  from { transform: translateY(0) rotate(0deg); }
  to { transform: translateY(3.5px) rotate(8deg); }
}
@keyframes care-drain {
  to { transform: translateY(4px) scaleY(.12); opacity: .3; }
}
@keyframes care-drip {
  0% { transform: translateY(0); opacity: 0; }
  25% { opacity: 1; }
  100% { transform: translateY(15px); opacity: 0; }
}
@keyframes care-roll {
  /* 30px — ровно до ног героя (центр мяча 101 → 71 при радиусе 11): дальше он
   * уезжал бы под корпус и читался бы пропавшим. Оборот ≈ 2πr ≈ 69px, поэтому
   * -160° почти без проскальзывания — качение видно, а не скольжение. */
  0% { transform: translateX(0) rotate(0deg); }
  100% { transform: translateX(-30px) rotate(-160deg); }
}
/* Сворачивание: squash к полу — герой поджимает лапы, а не проваливается.
 * `forwards` держит позу до конца визита: уснувший герой не должен вскакивать. */
@keyframes care-settle {
  0% { transform: translateY(0) scaleY(1) rotate(0deg); }
  55%, 100% { transform: translateY(3px) scaleY(.94) rotate(-3deg); }
}
@keyframes care-zzz {
  0% { transform: translateY(5px) scale(.6); opacity: 0; }
  30% { opacity: .9; }
  100% { transform: translateY(-13px) scale(1.1); opacity: 0; }
}
@keyframes care-nuzzle {
  from { transform: translateX(0) rotate(0deg); }
  to { transform: translateX(3.5px) rotate(-5deg); }
}
/* Глаза едут вверх к ладони: «следят» без второй пары зрачков и без JS. */
@keyframes care-gaze {
  from { transform: translateY(0); }
  to { transform: translateY(-1.7px); }
}
@keyframes care-blush {
  from { opacity: .55; }
  to { opacity: 1; }
}
@keyframes care-love {
  0% { transform: scale(.4) translateY(0); opacity: 0; }
  25% { transform: scale(1.1) translateY(-1px); opacity: 1; }
  60% { transform: scale(1) translateY(-3px); opacity: .9; }
  100% { transform: scale(1.15) translateY(-9px); opacity: 0; }
}

/* ---- 99.8 покой: герой не статуя ---------------------------------------- */
/* Мигание и хвост герою уже дали §25.4a и вид героя. В комнате добавляется
 * только редкое блуждание: раз в долгий цикл несколько шагов по ковру и назад.
 * Анимация стоит на самом SVG, поэтому data-care снимает её без :has() — в миг
 * заботы герой стоит там, где его кормят. */
.roomstage__hero .hero { animation: room-wander 38s ease-in-out infinite; }
.roomstage__hero .hero[data-care] { animation: none; }
@keyframes room-wander {
  0%, 16% { transform: translateX(0); }
  24%, 42% { transform: translateX(9%); }
  50%, 68% { transform: translateX(-7%); }
  76%, 100% { transform: translateX(0); }
}

/* ---- 99.9 меню заботы ---------------------------------------------------- */
/* Меню из пяти адресатов стоит на месте одной карточки дня. Тёплый тон комнаты
 * — amber, как у строки под сценой, — а не синий новостей: забота не событие,
 * и она не должна выглядеть вторым «двойным опытом».
 *
 * Сетка, а не строка: пять кнопок в один ряд на 358px дают по 65px — «Уложить
 * спать» там не читается. auto-fit поднимает до трёх колонок, и клетка остаётся
 * шире 90px; высота клетки — минимум 88px, что вдвое больше требуемых 44px
 * касания для toddler-пальца. */
.caremenu {
  margin-top: var(--s4); padding: var(--s3);
  border: 3px solid var(--amber-d); border-radius: var(--r-lg);
  background: var(--amber-t);
}
.caremenu__head {
  margin: 0 0 var(--s2);
  font-family: var(--font-display); font-weight: 800; font-size: var(--fs-body);
}
.caremenu__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
  gap: var(--s2);
}
.careitem {
  display: block; width: 100%; min-height: 88px;
  padding: var(--s2) var(--s1); margin: 0;
  border: 3px solid var(--amber-d); border-radius: var(--r-md);
  background: var(--surface);
  font: inherit; color: inherit; cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.careitem__tap {
  display: grid; justify-items: center; align-content: center; gap: var(--s1);
  width: 100%; min-height: 76px; padding: 0; border: 0; background: none;
  font: inherit; color: inherit; cursor: pointer;
}
.careitem__tap:active { transform: scale(.97); }
.careitem__emoji { font-size: var(--fs-title); line-height: 1; }
.careitem__name {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-caption); line-height: 1.2; text-align: center;
}
/* Сделано сегодня: клетка успокаивается — тот же размер, тише край и тон.
 * Ребёнок видит разницу, не читая. Только что сделанный — зелёный, как всё
 * «только что случилось» в этом продукте, и гаснет до тихого при следующем
 * заходе. Никакого «пропущено», никакого красного: забота не умеет долга. */
.careitem--done { border-color: var(--line); color: var(--ink-muted); }
.careitem.is-just { border-color: var(--green); color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
  .careitem__tap:active { transform: none; }
}

/* ---- 99.10 движение выключено: те же части, то же решение --------------- */
/* §25.7 и спокойный режим (M6) уже гасят .h-arm/.h-head/.h-root/.h-body/.h-eye
 * и сами группы [data-art]. Части реквизита — дети этих групп, и на них запрет
 * не падает, поэтому перечисляем их рядом — тем же !important, без новой руки. */
@media (prefers-reduced-motion: reduce) {
  .hero .p-food ellipse, .hero .p-water, .hero .p-drop,
  .hero .p-ball, .hero .p-zzz, .hero .p-heart,
  .roomstage__hero .hero {
    animation: none !important;
  }
}
html[data-calm] .hero .p-food ellipse, html[data-calm] .hero .p-water,
html[data-calm] .hero .p-drop, html[data-calm] .hero .p-ball,
html[data-calm] .hero .p-zzz, html[data-calm] .hero .p-heart,
html[data-calm] .roomstage__hero .hero {
  animation: none !important;
}

/* ======================== КОНЕЦ §99 HERO2 CARE =========================== */
