/* Шрифтов не подключаем: интерфейс рисуется системным (см. DESIGN.md §3).
   Geist-Variable и GeistMono-Variable удалены — 240 КБ, которые грузились
   на каждый первый визит и не использовались ни одним правилом. */

/* Apple Wallet-inspired system tokens. Light and dark remain user-facing names. */
:root,
[data-theme="light"] {
    --edge-inset: 1rem;
    --space-section: 1.25rem;
    /* Токены DESIGN.md. Имена оставлены прежними — на них смотрит
       tailwind.config.js, и переименование развалило бы каждый шаблон разом. */
    --background: 240 20% 96.5%;   /* #f5f5f7 */
    --foreground: 240 6% 12%;      /* #1d1d1f */
    --card: 0 0% 100%;
    --card-foreground: 240 6% 12%;
    --primary: 211 100% 45%;       /* #0071e3 — только заливка кнопок */
    --primary-foreground: 0 0% 100%;
    --secondary: 240 8% 93%;       /* #ececf0 */
    --secondary-foreground: 240 6% 12%;
    --muted: 240 8% 93%;
    --muted-foreground: 240 3% 45%;/* #6e6e73 */
    --tw-accent: 240 8% 93%;
    --tw-accent-foreground: 240 6% 12%;
    --destructive: 356 68% 47%;    /* #c9252d — расход */
    --destructive-foreground: 0 0% 100%;
    --success-hsl: 143 63% 30%;        /* #1d7f3e — приход */
    --warning: 36 100% 32%;        /* #a25c00 — разовое */
    --separator: 240 9% 84%;       /* #d2d2d7 */
    --border: 240 9% 84%;
    --input: 240 9% 78%;
    --ring: 211 100% 45%;
    /* Два радиуса на всю систему: 8px и pill. Третьего нет. */
    --radius: 8px;
    --radius-sm: 8px;
    --radius-pill: 980px;
    /* Шкала, из которой берутся все отступы и размеры компонентов. Пока её
       не было, каждый экран назначал свои: отступ карточки гулял между 14 и
       16px, кнопки набрались четырёх размеров. Глаз не читает это как выбор. */
    --card-pad: 13px;
    --gap-card: 9px;
    --gap-section: 16px;

    /* ── Доска ───────────────────────────────────────────────────────────
       Одна поверхность на всё приложение: карточки статистики, потока,
       года, квеста, кошельков, списков настроек и всего остального.
       Вид задан здесь пятью значениями, а правило ниже только раздаёт их.

       Смысл в том, что обложке — и любой будущей теме — достаточно
       переопределить эти пять строк, и сорок экранов меняются целиком.
       До этого каждый класс описывал границу и фон сам, и они разъезжались
       молча: отступ успел разойтись между 14 и 16 пикселями, а стекло
       обложки досталось семи классам из пятнадцати. */
    --board-bg: hsl(var(--card));
    --board-border: 1px solid hsl(var(--border));
    --board-radius: var(--radius);
    --board-shadow: none;
    --board-blur: none;
    --control-h: 36px;       /* кнопка и поле внутри карточки */
    --control-h-lg: 44px;    /* главное действие экрана и всё под палец */
    --control-pad: 16px;
    /* Шкала текста. Плотность задумывалась инженерной, но 14px базового на
       27-дюймовом мониторе — это не плотно, это мелко: строки сливаются, и
       экран читается дешевле, чем он есть. Размер теперь берётся отсюда и
       растёт вместе с экраном, а не задаётся в каждом правиле отдельно. */
    --text-body: 13.5px;
    --text-caption: 11.5px;
    --text-label: 10.5px;
    --text-section: 16px;
    --text-title: 21px;
    --text-hero: 34px;
    --text-amount: 21px;
    /* Единственная тень в системе — у того, что физически плавает над
       страницей: лист снизу, модалка, выпадающее меню (DESIGN.md §7).
       Токена не существовало, и каждая модалка изобретала свою. */
    --shadow-overlay: 0 12px 32px rgb(0 0 0 / .18);
    /* Системный шрифт: на маке и айфоне это SF Pro, отрисованный движком
       системы. Ничего не грузится и ничего не прыгает при загрузке. */
    --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;
    --font-display: -apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui, sans-serif;
    --font-rounded: ui-rounded, "SF Pro Rounded", -apple-system, system-ui, sans-serif;
    /* Все суммы — моноширинным, чтобы колонки стояли ровно. */
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
    /* The light canvas. It used to be a grey page under a *grey* glow — the
       dark theme gets a warm orange one — so every screen opened on smoke and
       the white cards read as unfinished rather than as cards. Cooler, lighter
       paper; the glow now has a colour instead of being dirt. */
    --app-bg: hsl(240 20% 97%);
    --app-surface: hsl(0 0% 100%);
    --app-surface-elevated: hsl(240 20% 98%);
    --app-border: hsl(240 10% 88%);
    --app-text: hsl(0 0% 3%);
    --app-muted: hsl(240 3% 39%);
    --app-income: hsl(145 63% 41%);
    --app-expense: hsl(351 100% 61%);
    --app-nav-active-bg: hsl(0 0% 0%);
    --app-nav-active-fg: hsl(0 0% 100%);
}

@media (min-width: 768px) {
    .page-dashboard {
        --edge-inset: 1.5rem;
        --space-section: 1.5rem;
    }
}
[data-theme="dark"],
.dark {
    --background: 0 0% 0%;
    --foreground: 240 20% 96.5%;   /* #f5f5f7 */
    --card: 240 3% 11%;            /* #1c1c1e */
    --card-foreground: 240 20% 96.5%;
    --primary: 211 100% 52%;       /* #0a84ff — синий, который держит чёрный */
    --primary-foreground: 0 0% 100%;
    --secondary: 240 2% 17%;       /* #2c2c2e */
    --secondary-foreground: 240 20% 96.5%;
    --muted: 240 2% 17%;
    --muted-foreground: 240 4% 61%;/* #98989f */
    --tw-accent: 240 2% 17%;
    --tw-accent-foreground: 240 20% 96.5%;
    --destructive: 3 100% 64%;     /* #ff453a */
    --destructive-foreground: 0 0% 100%;
    --success-hsl: 142 65% 51%;        /* #30d158 */
    --warning: 50 100% 52%;        /* #ffd60a */
    --separator: 240 2% 23%;       /* #38383a */
    --border: 240 2% 23%;
    --input: 240 2% 29%;           /* #48484a */
    --ring: 211 100% 52%;
    --app-bg: hsl(0 0% 2%);
    --app-surface: hsl(240 3% 10%);
    --app-surface-elevated: hsl(240 3% 14%);
    --app-border: rgb(255 255 255 / .09);
    --app-text: hsl(0 0% 98%);
    --app-muted: rgb(255 255 255 / .56);
    --app-income: hsl(145 70% 54%);
    --app-expense: hsl(351 100% 67%);
    --app-nav-active-bg: hsl(0 0% 100%);
    --app-nav-active-fg: hsl(0 0% 0%);
}

* { border-color: hsl(var(--border)); }
html {
    height: 100%;
    overflow-x: hidden;
    /* Утилиты Tailwind заданы в rem и смотрят сюда, а не на родителя:
       поднять кегль на контейнере страницы бесполезно, text-sm останется
       тем же. Поэтому шкала экрана задаётся здесь — и вместе с текстом
       растут отступы, высоты и радиусы, заданные в rem. */
    font-size: 14.5px;
}
@media (min-width: 768px)  { html { font-size: 17px; } }
@media (min-width: 1280px) { html { font-size: 18px; } }
body { font-family: var(--font-sans); -webkit-font-smoothing: antialiased; }
h1, h2, h3, h4, h5, h6, .font-display { font-family: var(--font-display); }
button, input, select, textarea { font-family: inherit; }
/* Day header in the transaction list — same treatment as the dashboard feed,
   because it is the same object shown in two places. */
.tx-day-head span:last-child { letter-spacing: 0; text-transform: none; }

/* On a true-black canvas an 11%-lightness fill is a 1.23:1 step — the card
   edge is invisible and everything reads as one flat sheet. Apple Wallet
   solves this with heavy shadow rather than a lighter fill, so the card stays
   dark and still floats. */

.tabular-nums, .needt-mobile-balance-amount, .needt-metric-amount,
.wallet-detail-amount, .wallet-card-balance, .needt-goal-amount,
.needt-ordinary-amount, .needt-stats-amount, .needt-donut-centre strong {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1, "ss01" 1;
}
body {
    background-color: hsl(var(--background));
    background-image: none;
    overflow-x: hidden;
}
.dark body {
    background-image: none;
}

@media (max-width: 767.98px) {
    .ui-modal-overlay { align-items:flex-end; padding:0; }
    .ui-modal { max-width:none; border-radius: var(--radius) var(--radius) 0 0; padding-bottom:calc(1.5rem + env(safe-area-inset-bottom, 0px)); }
    .action-confirm-sheet { padding-top: 1.1rem; }
    /* Холст без свечения: DESIGN.md §11 запрещает градиенты, глубину даёт
       сдвиг поверхности и граница в 1px. Персиковая дымка поверх чёрной
       иконки и белых карточек читалась как подкрашенный экран. */
    html body {
        min-height: 100vh;
        background: var(--app-bg);
    }
    body:not(.page-dashboard) main {
        min-height: 100vh;
        padding-top: max(.85rem, env(safe-area-inset-top, 0px));
    }
    /* `.field-bare` is the escape hatch for an input that is not a field: the
       search pills draw their own border and background on the wrapper, and
       this rule — three classes deep — used to win against them and paint a
       second rectangle inside the pill. */
    body:not(.page-dashboard) input:not([type="checkbox"]):not([type="radio"]):not(.field-bare),
    body:not(.page-dashboard) select,
    body:not(.page-dashboard) textarea {
        border-color: var(--app-border);
        background-color: var(--app-surface-elevated);
        color: var(--app-text);
    }
}
.tabular-nums,
input[type="number"],
input[inputmode="decimal"] {
    font-variant-numeric: tabular-nums;
}
.rounded-xl { border-radius: var(--radius) !important; }
.rounded-lg { border-radius: var(--radius) !important; }
/* Тени отменены (DESIGN.md §7): глубину даёт сдвиг поверхности и граница
   в 1px. Класс оставлен — на него ссылаются десятки шаблонов, — но
   ничего не рисует. */

button {
    min-height: 2.75rem;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}
button:not(:disabled) {
    transition-property: transform, background-color, border-color, color, opacity;
    transition-duration: 160ms;
    transition-timing-function: cubic-bezier(.32,.72,0,1);
}
button:not(:disabled):active { transform: scale(.97); }
a { -webkit-tap-highlight-color: transparent; }
body.overflow-hidden .mobile-nav-wrap { display: none; }

/* Reduce rubber-band overscroll past the page on phones (especially iOS) */
@media (max-width: 767.98px) {
    html {
        overscroll-behavior-y: none;
    }
    body {
        overscroll-behavior-y: none;
    }
}

/* Card component */
/* Поверхность приходит из правила доски ниже — здесь только цвет текста. */

/* Input */

/* Badge */
.ui-badge { display: inline-flex; align-items: center; border-radius: 9999px; padding: 0.125rem 0.625rem; font-size: 0.75rem; font-weight: 500; line-height: 1.25rem; }

/* Modal / Dialog (above mobile bottom nav z-50 and add sheet z-60) */
.ui-modal-overlay {
    position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 300; display: flex; align-items: center; justify-content: center; padding: 1rem;
    opacity: 0; transition: opacity 0.15s;
}
.ui-modal-overlay.hidden { display: none; }
.ui-modal-overlay:not(.hidden) { opacity: 1; }
.ui-modal {
    background: hsl(var(--card)); border-radius: var(--radius); box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
    width: 100%; max-width: 28rem; max-height: 90vh; overflow-y: auto; padding: 1.5rem;
    transition: transform 400ms cubic-bezier(.32,.72,0,1), opacity 250ms ease;
}

/* Safe area for mobile bottom nav */

/* Premium mobile bottom nav */

/* Wallet building blocks */
.sort-row-copy strong {
    display: block;
    overflow: hidden;
    font-size: .875rem;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sort-row-copy small {
    display: block;
    color: hsl(var(--muted-foreground));
    font-size: .6875rem;
}

/* One merchant, one decision. The row splits into two lines on purpose: the
   name and money on top, the choice underneath, so a long bank string never
   squeezes the select down to nothing. */
/* Решение не приняли — строка осталась и об этом сказано движением, а не
   молчанием. Раньше отказ выглядел как успех: строка уезжала с экрана и
   возвращалась только после перезагрузки. */
/* Выключенное движение здесь не оговаривается: общий ответ на
   prefers-reduced-motion в этом файле один и гасит анимации через
   !important. Второй такой блок и есть то, что ловит тест. */
@keyframes sort-refused {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-5px); }
    75% { transform: translateX(5px); }
}
/* The whole head is the button that opens the category sheet: on a phone a
   32px "Other…" link was the only way to reach the full list, and it was the
   smallest target on the screen. Now the row itself is the target. */
.sort-merchant-copy strong {
    display: block;
    overflow: hidden;
    font-size: .875rem;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sort-merchant-copy small {
    display: block;
    color: hsl(var(--muted-foreground));
    font-size: .6875rem;
}
.sort-suggest span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The contour sits beside the suggestion, not across the row: as a
   width:100% block it squeezed the suggestion chip to zero and the one-tap
   "yes" became an invisible sliver. */

/* Category sheet ---------------------------------------------------------- */
.cat-sheet { position: fixed; inset: 0; z-index: 10060; }
.cat-sheet.hidden { display: none; }
.cat-sheet-backdrop {
    position: absolute;
    inset: 0;
    background: rgb(0 0 0 / .55);
}
.cat-sheet-panel {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    max-height: 78vh;
    overflow-y: auto;
    border-radius: var(--radius) var(--radius) 0 0;
    background: hsl(var(--card));
    padding: 1rem 1rem calc(1.25rem + env(safe-area-inset-bottom));
}
.cat-sheet-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: .875rem;
}
.cat-sheet-head strong {
    overflow: hidden;
    font-size: .9375rem;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.cat-sheet-close {
    flex: none;
    width: 2rem;
    height: 2rem;
    border: 0;
    border-radius: 999px;
    background: hsl(var(--muted, var(--background)));
    color: inherit;
    font-size: 1.1rem;
    line-height: 1;
}
.cat-sheet-grid {
    display: grid;
    gap: .5rem;
    grid-template-columns: repeat(auto-fill, minmax(5.25rem, 1fr));
}
.cat-sheet-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .4rem;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    padding: .6rem .25rem;
    color: inherit;
    font-size: .6875rem;
    line-height: 1.25;
    text-align: center;
}
.cat-sheet-tile:active { background: hsl(var(--muted, var(--background))); }

/* ── Выбор категории на весь экран ────────────────────────────────────────
   Нижний лист закрывал полэкрана на телефоне и полоской лежал на десктопе.
   Теперь одно и то же везде: фон страницы проявляется поверх содержимого,
   плитки — поверх фона. Анимируются только opacity и transform. */
.cat-picker .cat-sheet-backdrop {
    background: hsl(var(--background) / .97);
    opacity: 0;
    transition: opacity 220ms ease;
}
.cat-picker-body {
    position: absolute;
    inset: 0;
    overflow-y: auto;
    padding: max(1.5rem, env(safe-area-inset-top, 0px)) 1.25rem calc(2rem + env(safe-area-inset-bottom, 0px));
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 220ms ease, transform 260ms cubic-bezier(.2, .8, .2, 1);
}
.cat-picker.is-open .cat-sheet-backdrop,
.cat-picker.is-open .cat-picker-body { opacity: 1; transform: none; }
.cat-picker-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    max-width: 920px;
    margin: 0 auto 1.5rem;
}
.cat-picker-head strong { font-size: 1.25rem; font-weight: 600; letter-spacing: -.01em; }
.cat-picker .cat-sheet-close {
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
}
.cat-picker-sections { display: flex; flex-direction: column; gap: 1.75rem; max-width: 920px; margin: 0 auto; }
/* Первой идёт секция, которая подходит операции. */
.cat-picker[data-type="income"] [data-section="income"] { order: -1; }
.cat-picker-label {
    margin: 0 0 .75rem;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: hsl(var(--muted-foreground));
}
.cat-picker .cat-sheet-grid { grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr)); gap: .5rem; }
.cat-picker .cat-sheet-tile {
    padding: .75rem .25rem;
    font-size: .75rem;
    cursor: pointer;
    transition: background-color 120ms ease;
}
.cat-picker .cat-sheet-tile:hover { background: hsl(var(--foreground) / .06); }
.cat-sheet-tile.is-current { background: hsl(var(--foreground) / .1); box-shadow: inset 0 0 0 1px hsl(var(--foreground) / .25); }

/* Кнопка выбора в листе добавления — одна строка вместо ленты чипов. */
.cat-pick-mark > span { width: 2rem; height: 2rem; border-radius: .5rem; }
.sort-empty p {
    margin-top: .875rem;
    color: hsl(var(--muted-foreground));
    font-size: .8125rem;
    line-height: 1.5;
}

/* PostFinance sync — the same card language as the dashboard balance block:
   one oversized figure that answers the only question the page is asked
   ("when did this last work?"), then the steps underneath. */
.sync-step-body strong { font-size: .9375rem; }
.sync-step-body span {
    font-size: .8125rem;
    line-height: 1.5;
    color: hsl(var(--muted-foreground));
}

/* The command has to survive being pasted, so it must never be visually
   truncated — it wraps instead of scrolling out of sight. */
.sync-command code {
    display: block;
    font-size: .75rem;
    line-height: 1.5;
    word-break: break-all;
    color: hsl(var(--foreground));
}
.sync-details summary {
    padding: .5rem 0;
    font-size: .875rem;
    font-weight: 600;
    cursor: pointer;
}
.sync-details-body p {
    margin: 0 0 .6rem;
    font-size: .8125rem;
    line-height: 1.5;
    color: hsl(var(--muted-foreground));
}
.sync-details-body code {
    font-size: .75rem;
    word-break: break-all;
}
.wallet-category-tile span { max-width:5rem; overflow:hidden; text-overflow:ellipsis; }
.wallet-search-field input { min-width:0; flex:1; border:0; outline:0; background:transparent; color:hsl(var(--foreground)); font-size:1rem; }

:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }

/* ── Шкала по ширине экрана ──────────────────────────────────────────────
   Один набор чисел на телефон, другой на десктоп. Раньше десктоп получал
   телефонные размеры и выглядел уменьшенной копией приложения, а не
   отдельным экраном. */
@media (min-width: 768px) {
    :root, [data-theme="light"], [data-theme="dark"] {
        --card-pad: 20px;
        --gap-card: 14px;
        --gap-section: 24px;
        --control-h: 40px;
        --control-h-lg: 48px;
        --control-pad: 20px;
        --text-body: 15px;
        --text-caption: 13px;
        --text-label: 12px;
        --text-section: 19px;
        --text-title: 28px;
        --text-hero: 52px;
        --text-amount: 28px;
    }
}
@media (min-width: 1280px) {
    :root, [data-theme="light"], [data-theme="dark"] {
        --card-pad: 22px;
        --gap-card: 16px;
        --text-body: 16px;
        --text-caption: 13px;
        --text-section: 20px;
        --text-title: 30px;
        --text-hero: 58px;
        --text-amount: 30px;
    }
}

/* Заголовок не оставляет одно слово на последней строке, абзац не оставляет
   висячую строку. Обе подсказки безопасно игнорируются там, где их нет. */
h1, h2, h3, .needt-goal-amount { text-wrap: balance; }
p { text-wrap: pretty; }

/* Smooth transitions for theme */
body, .ui-card, header, nav { transition: background-color 0.2s, color 0.2s, border-color 0.2s; }

/* Active bottom nav highlight */

/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: hsl(var(--muted)); border-radius: var(--radius); }

/* Animations */
@keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.animate-fade-up { animation: fadeUp 0.35s ease-out; }

/* Page transitions */
.page-transition { animation: pageFade 0.25s ease-out; }
@keyframes pageFade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }

/* Bottom nav active highlight */

/* ── Статистика: круговая и выбор категорий ─────────────────────────────── */
/* Карточка объявлена один раз. До этого одно и то же — граница, радиус,
   поверхность, отступ — было описано в семи местах, и отступ успел разойтись
   между 16px и 14px. Разница не читается как решение, она читается как
   небрежность: именно из таких расхождений экран выглядит дешёвым. */
/* Список полный намеренно. Любая коробка, которая на экране читается как
   отдельная плоскость, стоит здесь и берёт вид из токенов --board-*.
   Своего фона и своей границы у этих классов больше нет: тот, кто заведёт
   новую коробку мимо списка, увидит её белой и придёт сюда.

   Чего здесь нет — отступа. У досок он честно разный: у списка настроек
   его нет вовсе, ряды отбивают себя сами. Общий padding пришлось бы
   отменять в половине мест, и «один токен» превратился бы в ложь. */
.board,
.ui-card,
.ios-list,
.wallet-card,
.wallet-summary-tile,
.wallet-search-field,
.needt-stats-card,
.needt-flow,
.needt-year,
.needt-quest-card,
.needt-quest-prompt,
.needt-ordinary,
.needt-desk-goal {
    border: var(--board-border);
    border-radius: var(--board-radius);
    background: var(--board-bg);
    box-shadow: var(--board-shadow);
    backdrop-filter: var(--board-blur);
    -webkit-backdrop-filter: var(--board-blur);
}
.needt-stats-card,
.needt-flow,
.needt-year,
.needt-quest-card,
.needt-quest-prompt,
.needt-ordinary,
.needt-desk-goal { padding: var(--card-pad); }
/* Выбранный чип заливается цветом текста, а не акцентом: акцент занят
   кнопками (DESIGN.md §1). */
.needt-chip.is-on { background: hsl(var(--foreground)); border-color: hsl(var(--foreground)); color: hsl(var(--background)); }
.needt-donut-centre strong {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    /* Дырка в кольце — около 120px. «5,534.63 CHF» в 17px заходило на
       сектора, в 15px помещается с запасом. */
    font-size: 15px;
    font-weight: 600;
    letter-spacing: -.012em;
}
.needt-donut-centre span { font-size: 11px; color: hsl(var(--muted-foreground)); }
.needt-stats-note { margin: .75rem 0 0; font-size: var(--text-caption); color: hsl(var(--muted-foreground)); }

/* 150px хватало на имя без суммы; с суммой рядом имя резалось до «F…» и
   «Ap…» — выбрать категорию по такой подписи нельзя. */
.needt-pick.is-on { border-color: hsl(var(--foreground)); }
.needt-pick input { width: 15px; height: 15px; flex: none; accent-color: hsl(var(--foreground)); }

/* ── Кнопки ──────────────────────────────────────────────────────────────
   Три вида и две высоты, больше ничего. Раньше их было четыре размера на
   трёх радиусах: 14px в главной, 13px в тихой, 13px/999px в ставке и 48px
   без границы в кошельках. Одна и та же вещь, four разных ответа — и экран
   читается как собранный из кусков.

   Основная — заливка акцентом, на экране одна. Вторичная — та же форма,
   граница вместо заливки. Тихая — без фона, для действий в строке списка.
   Все три: pill, 14px/600, высота из шкалы, зона нажатия не меньше 44px. */
.needt-button-primary,
.needt-button-secondary,
.needt-button-quiet {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .375rem;
    min-height: var(--control-h);
    border-radius: var(--radius-pill);
    padding: 0 var(--control-pad);
    font-size: var(--text-body);
    font-weight: 600;
    line-height: 1;
    letter-spacing: -.014em;
    white-space: nowrap;
    cursor: pointer;
    /* Только цвет и граница. Тени, размеры и позиции не анимируются:
       первое дёргает раскладку, второе читается как демо. */
    transition: background-color 120ms cubic-bezier(.32,.72,0,1),
                border-color 120ms cubic-bezier(.32,.72,0,1),
                color 120ms cubic-bezier(.32,.72,0,1);
}
/* Палец попадает в 44px, даже когда кнопка нарисована в 36: растёт зона
   нажатия, а не сама кнопка. */
.needt-button-primary::after,
.needt-button-secondary::after,
.needt-button-quiet::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
    height: var(--control-h-lg);
    transform: translateY(-50%);
}
.needt-button-primary,
.needt-button-secondary,
.needt-button-quiet { position: relative; }

.needt-button-primary {
    border: 1px solid hsl(var(--primary));
    background: hsl(var(--primary));
    color: hsl(var(--primary-foreground));
}
.needt-button-primary:hover { background: hsl(var(--primary) / .88); border-color: hsl(var(--primary) / .88); }
.needt-button-primary:active { background: hsl(var(--primary) / .78); }

.needt-button-quiet {
    border: 1px solid hsl(var(--border));
    background: transparent;
    color: hsl(var(--foreground));
}
.needt-button-quiet:hover { background: hsl(var(--secondary)); }
.needt-button-quiet:active { background: hsl(var(--secondary)); }

/* Отключённое гасит текст, а не всю кнопку: полупрозрачный элемент поверх
   карточки читается как сбой отрисовки, а не как запрет. */
.needt-button-primary:disabled,
.needt-button-secondary:disabled,
.needt-button-quiet:disabled {
    color: hsl(var(--muted-foreground));
    background: hsl(var(--secondary));
    border-color: hsl(var(--border));
    cursor: not-allowed;
}
.needt-button-lg { min-height: var(--control-h-lg); }

/* Шестое состояние — «отправляется». Было нарисовано пять: обычное,
   наведение, нажатие, фокус, отключённое. Без шестого кнопка после тапа
   выглядела так же, как до него, и на медленной сети её жали второй раз —
   второй «+» в квесте, вторая запись. Ставит его main.js на время отправки
   формы (aria-busy="true"). Размер кнопки не меняется: текст становится
   прозрачным, по центру крутится кольцо цвета текста. ::after занят зоной
   нажатия, поэтому кольцо — ::before. */
.needt-button-primary { --busy-ring: hsl(var(--primary-foreground)); }

.needt-button-quiet { --busy-ring: hsl(var(--foreground)); }
[aria-busy="true"]:is(.needt-button-primary, .needt-button-secondary, .needt-button-quiet,
                      .aurora-chip, .aurora-step, .sort-chip, .sort-suggest) {
    position: relative;
    color: transparent !important;
    pointer-events: none;
    cursor: progress;
}
[aria-busy="true"]:is(.needt-button-primary, .needt-button-secondary, .needt-button-quiet,
                      .aurora-chip, .aurora-step, .sort-chip, .sort-suggest)::before {
    content: "";
    position: absolute;
    inset: 0;
    width: 1em;
    height: 1em;
    margin: auto;
    border-radius: 50%;
    border: 2px solid var(--busy-ring, hsl(var(--foreground)));
    border-right-color: transparent;
    animation: needt-busy-spin .7s linear infinite;
}
@keyframes needt-busy-spin { to { transform: rotate(360deg); } }
/* Уменьшенное движение отвечает общее правило: кольцо стоит на месте,
   но по-прежнему видно — полукольцо само говорит «жду». */

.needt-goal-label,
.needt-ordinary-label {
    margin: 0 0 .25rem;
    font-size: var(--text-label);
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: hsl(var(--muted-foreground));
}
/* Суммы живут на трёх ступенях: 40 — главная цифра экрана, 24 — цифра
   карточки, 17 — сумма в строке. Пятая ступень была в mobile-balance
   (clamp до 75px, вес 720, трекинг −.065em) — она и читалась дешевле
   всего: вес выше 600 DESIGN.md не допускает, а зажатый трекинг на
   крупном кегле выглядит сдавленным. */
.needt-goal-amount,
.needt-quest-score,
.needt-ordinary-amount,
.needt-stats-amount {
    margin: 0;
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: hsl(var(--foreground));
}
.needt-goal-amount {
    font-size: var(--text-hero);
    line-height: 1.05;
    letter-spacing: -.021em;
}

.needt-goal-meta {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem 1rem;
    margin: .5rem 0 0;
    font-size: var(--text-caption);
    color: hsl(var(--muted-foreground));
    font-variant-numeric: tabular-nums;
}
/* Падение трат — хорошая новость, и она произносится вслух. */

/* ── Заработок против трат по неделям ──────────────────────────────────
   Единственный график, на котором видно направление, а не факт. Оба ряда
   на одной шкале: раздельные шкалы врут о соотношении. */

.needt-flow-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.needt-flow-legend { display: flex; gap: .875rem; margin: 0; font-size: 12px; color: hsl(var(--muted-foreground)); }
.needt-flow-key { display: inline-block; width: 8px; height: 8px; border-radius: var(--radius); margin-right: .35rem; }
.needt-flow-key--earned { background: hsl(var(--success-hsl)); }
.needt-flow-key--spent { background: hsl(var(--destructive)); }

/* ── Боковое меню, десктоп ───────────────────────────────────────────────
   Семь равнозначных кнопок в ряд в шапке — это отсутствие иерархии: глазу
   не за что зацепиться, и каждый заход начинался с чтения всех семи. Здесь
   пункты сгруппированы по тому, зачем в них заходят, активный выделен
   заливкой текстом, а не акцентом: акцент занят кнопками (DESIGN.md §1). */
.needt-side-item.is-active {
    background: hsl(var(--foreground));
    color: hsl(var(--background));
    font-weight: 600;
}
.needt-side-item.is-active .needt-side-badge { background: hsl(var(--background)); color: hsl(var(--foreground)); }

/* Сдвигается вся страница, а не отдельные блоки: `margin-left` на main при
   `w-full` даёт ширину 100% плюс отступ, то есть вылет за правый край. */
/* Mobile-first dashboard prototype — intentionally isolated from desktop. */
@media (max-width: 767.98px) {
    .page-app {
        --edge-inset: 1rem;
        --space-section: 1.25rem;
        --needt-mobile-panel: color-mix(in srgb, var(--app-surface) 90%, transparent);
        --needt-mobile-border: var(--app-border);
        --needt-mobile-muted: var(--app-muted);
        background: var(--app-bg);
        color: var(--app-text);
    }
    .page-app main {
        max-width: none;
        padding-top: 0;
        padding-bottom: 1rem;
    }
    /* Same column as the dashboard for every other app screen. */
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}

/* Print */
@media print {  header, nav, .no-print { display: none !important; } body { padding-bottom: 0 !important; } }

/* Expected returns: the inline form inside a grouped list.
   <details> keeps it collapsed without any JavaScript; the default marker is
   removed so the summary reads as an ordinary iOS row. */
.expected-return-form > summary::-webkit-details-marker { display: none; }
.expected-return-form > summary::marker { content: ""; }
.expected-return-form > summary { font-weight: 500; }
.expected-return-form[open] > summary { color: hsl(var(--muted-foreground)); }
.expected-return-form input[type="number"],
.expected-return-form input[type="text"],
.expected-return-form input[type="date"] {
    color: inherit;
    font: inherit;
}

/* ---------------------------------------------------------------------------
   iOS-style grouped settings. Apple's inset list: rounded group, hairline
   separators that stop short of the icon, chevron on every row that navigates,
   section header above rather than a card title inside.
   --------------------------------------------------------------------------- */
.ios-row-copy strong { display: block; font-weight: 500; }
.ios-row-copy small {
    display: block;
    color: hsl(var(--muted-foreground));
    font-size: .75rem;
}

/* The "moved" chip has no category tile in front of it, so it needs its own
   padding — the suggestion pill reserves room on the left for a 28px icon. */

/* ---------------------------------------------------------------------------
   Earning pressure. Sits directly under the balance, because the balance is
   the one number he opens the app for and these are the ones that argue with
   it. Red, not because something is broken, but because nothing is happening.
   --------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   One amount, one appearance. Colour says what the money did — the same four
   answers the row backgrounds give — and the sign is always printed, because
   a white number with a hyphen in front of it is a number you have to decode.
   --------------------------------------------------------------------------- */
.money--in { color: hsl(145 70% 42%); }
.dark .money--in { color: hsl(145 79% 60%); }

/* Picker sheet: rows rather than tiles, because the thing being chosen is a
   transaction and a transaction needs a date, a wallet and an amount. */
.tx-picker-hint {
    margin: -.35rem 0 .75rem;
    color: hsl(var(--muted-foreground));
    font-size: .75rem;
    line-height: 1.45;
}
.tx-picker-list { display: flex; flex-direction: column; }
.tx-picker-row {
    display: flex;
    width: 100%;
    align-items: center;
    gap: .625rem;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    padding: .6rem .5rem;
    color: inherit;
    text-align: left;
}
.tx-picker-row:active { background: hsl(var(--muted, var(--background))); }
.tx-picker-copy { min-width: 0; flex: 1; }
.tx-picker-copy strong {
    display: block;
    overflow: hidden;
    font-size: .875rem;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.tx-picker-copy small {
    display: block;
    color: hsl(var(--muted-foreground));
    font-size: .6875rem;
}
.tx-picker-amount {
    flex: none;
    font-size: .875rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.tx-picker-empty {
    padding: 1.25rem .5rem;
    color: hsl(var(--muted-foreground));
    font-size: .8125rem;
    text-align: center;
}

/* ---------------------------------------------------------------------------
   Chat — the one permanent thread.

   Full height with no bottom bar, because this is a room you enter rather than
   a tab you glance at. The glow is the dashboard's, so the two screens read as
   the same app rather than two products.
   --------------------------------------------------------------------------- */
.page-chat, .page-chat body { overflow: hidden; }
/* The screen paints its own gradient edge to edge. Without reaching <html>
   too, the app background showed as a band above the notch. */
.page-chat { background: transparent; }
html:has(body.page-chat) { background: rgb(24 12 34); }
/* !important earns its place exactly once: this screen owns the whole
   viewport, and <main>'s utility padding put a band above the notch that no
   ordinary override removed. */
.page-chat main { max-width: none; padding: 0 !important; height: 100dvh; overflow: hidden; }
.page-chat > header { display: none; }
.page-chat .page-transition { animation: none; transform: none; }
.chat-title strong { display: block; font-size: .9375rem; }
.chat-title small { display: block; color: var(--app-muted); font-size: .6875rem; }

/* Weed's problem list: the front page of the record. */
.chat-problem.is-active {
    background: rgb(255 255 255 / .14);
    color: var(--app-text);
    font-weight: 600;
}
.chat-hero h1 {
    font-size: 2rem;
    font-weight: 600;
    letter-spacing: -.02em;
    line-height: 1.12;
}
.chat-hero p {
    margin-top: .875rem;
    max-width: 22rem;
    color: var(--app-muted);
    font-size: .8125rem;
    line-height: 1.5;
}
.chat-msg {
    max-width: min(85%, 30rem);
    border-radius: var(--radius);
    padding: .7rem .875rem;
    font-size: .875rem;
    line-height: 1.45;
}
/* Ten days at the counter and nobody collected it. */
.chat-msg.is-stale { opacity: .6; }
.chat-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
/* The consult's own question, kept apart from the prose so the answer has
   something to be an answer to. */

.chat-context { margin-top: .45rem; font-size: .6875rem; }
.chat-context summary { cursor: pointer; opacity: .7; }
.chat-context dl {
    display: grid;
    gap: .15rem;
    margin: .4rem 0 0;
    grid-template-columns: 1fr;
}
.chat-context div { display: flex; justify-content: space-between; gap: .75rem; }
.chat-context dt { opacity: .65; }
.chat-context dd { margin: 0; font-variant-numeric: tabular-nums; }
.chat-context-list { flex-direction: column; }
.chat-context-list dd { display: flex; flex-direction: column; opacity: .85; }
.chat-cats {
    display: flex;
    gap: .35rem;
    overflow-x: auto;
    padding-bottom: .75rem;
    scrollbar-width: none;
}
.chat-cats::-webkit-scrollbar { display: none; }
.chat-cat {
    flex: none;
    border: 0;
    border-radius: 999px;
    background: none;
    /* 19px wide was the whole tap target for "All". */
    padding: .25rem .6rem;
    color: var(--app-muted);
    font-size: .9375rem;
    white-space: nowrap;
}
.chat-cat.is-active {
    background: rgb(255 255 255 / .12);
    color: var(--app-text);
    font-weight: 600;
}
.chat-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: .2rem;
    border: 1px solid rgb(255 255 255 / .14);
    border-radius: var(--radius);
    background: hsl(var(--card));
    padding: .875rem;
    color: inherit;
    text-align: left;
}
.chat-card[hidden] { display: none; }
.chat-card strong { font-size: .875rem; font-weight: 600; }
.chat-card small { color: var(--app-muted); font-size: .6875rem; line-height: 1.3; }

.chat-chips {
    display: flex;
    gap: .4rem;
    overflow-x: auto;
    padding: 0 1rem .5rem;
    scrollbar-width: none;
}
.chat-chips::-webkit-scrollbar { display: none; }
.chat-chip {
    flex: none;
    border: 1px solid hsl(var(--border));
    border-radius: 999px;
    background: hsl(var(--secondary));
    padding: .35rem .8rem;
    color: inherit;
    font-size: .75rem;
    font-weight: 500;
    white-space: nowrap;
}
.chat-chip:active { background: hsl(var(--secondary)); }
.chat-composer textarea {
    min-width: 0;
    max-height: 8.75rem;
    flex: 1;
    border: 0;
    background: transparent;
    padding: .55rem 0;
    color: inherit;
    font: inherit;
    font-size: .9375rem;
    line-height: 1.4;
    outline: none;
    resize: none;
}
.chat-composer textarea::placeholder { color: var(--app-muted); }
.chat-composer-extra, .chat-send {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 999px;
    color: inherit;
}
.chat-send {
    width: 2.85rem;
    height: 2.85rem;
    background: hsl(var(--primary));
    color: hsl(var(--primary-foreground));
}

/* Jump to the newest instead of yanking him down while he is reading. */

@media (min-width: 768px) {
    .chat-body, .chat-header, .chat-problems, .chat-composer, .chat-chips {
        margin: 0 auto;
        width: min(100%, 46rem);
    }
}

/* ---------------------------------------------------------------------------
   Transaction screen. The order is the design: what it is, what to do about
   it, then the reference, then Delete.
   --------------------------------------------------------------------------- */
.tx-hero h1 { margin-top: .5rem; font-size: 1.125rem; font-weight: 600; }

/* Two states, one control: the iOS segmented control, because "Spent" and
   "Moved" are one answer with two values rather than two buttons. */
.tx-segment-option.is-active {
    background: hsl(var(--primary));
    color: hsl(var(--primary-foreground, 0 0% 100%));
}
.tx-details summary::-webkit-details-marker { display: none; }

/* The picker searches, and says so. */
.tx-picker-search {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-bottom: .5rem;
    border: 1px solid hsl(var(--border));
    border-radius: 999px;
    background: hsl(var(--muted, var(--background)));
    padding: .5rem .875rem;
}
.tx-picker-search input {
    min-width: 0;
    flex: 1;
    border: 0;
    background: transparent;
    color: inherit;
    font-size: .875rem;
    outline: none;
    /* Both halves of the double rectangle: WebKit's own searchfield box, and
       the mobile input rule above — which `.field-bare` on the element now
       excludes. Either one alone drew a box inside the pill. */
    -webkit-appearance: none;
    appearance: none;
}
.tx-picker-search input::-webkit-search-decoration,
.tx-picker-search input::-webkit-search-cancel-button,
.tx-picker-search input::-webkit-search-results-button,
.tx-picker-search input::-webkit-search-results-decoration {
    -webkit-appearance: none;
    display: none;
}
.tx-picker-search:focus-within {
    border-color: color-mix(in srgb, hsl(var(--primary)) 55%, transparent);
}
/* The category strip scrolls, and the last tile used to be sliced clean in
   half at the edge with nothing saying there was more. A fade is the only
   honest edge: it reads as "continues" rather than as "cut". */

/* Why a row is in the list, when it is not an exact match. Small and beside
   the name, so an approximate offer never looks like a certain one. */
.tx-picker-why {
    margin-left: .4rem;
    border-radius: 999px;
    background: color-mix(in srgb, hsl(var(--muted-foreground)) 18%, transparent);
    padding: .05rem .35rem;
    color: hsl(var(--muted-foreground));
    font-size: .625rem;
    font-style: normal;
    font-weight: 600;
    vertical-align: middle;
}

/* The wallet the row is already in. Marked rather than hidden: seeing where it
   sits now is half the reason the sheet gets opened. */
.tx-picker-row.is-current {
    background: color-mix(in srgb, hsl(var(--primary)) 10%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, hsl(var(--primary)) 30%, transparent);
}
.tx-picker-more {
    display: block;
    width: 100%;
    border: 0;
    background: transparent;
    padding: .6rem;
    color: hsl(var(--primary));
    font-size: .8125rem;
}

/* ---------------------------------------------------------------------------
   Chat motion.

   One rule decides all of it: motion here reports something. A message rising
   into place says it was accepted; a chip leaving says it was used; the glow
   breathing says the thread is live and nobody has answered yet. Nothing moves
   to be pretty, because on a screen you open twenty times a day decoration
   becomes a tax.

   Distances stay under 12px and durations under 260ms — far enough to be read
   as motion, short enough that a fast tap never waits for it. Everything is
   transform and opacity, which the compositor handles without a repaint.
   --------------------------------------------------------------------------- */
@keyframes chat-rise {
    from { opacity: 0; transform: translate3d(0, 10px, 0) scale(.985); }
    to   { opacity: 1; transform: none; }
}
@keyframes chat-fade-down {
    from { opacity: 0; transform: translate3d(0, -6px, 0); }
    to   { opacity: 1; transform: none; }
}
@keyframes chat-pop {
    0%   { opacity: 0; transform: scale(.9); }
    60%  { transform: scale(1.03); }
    100% { opacity: 1; transform: none; }
}
@keyframes chat-sheet-up {
    from { transform: translate3d(0, 14px, 0); opacity: 0; }
    to   { transform: none; opacity: 1; }
}
/* Slow and wide enough to read as light rather than as an animation. */

/* Rows arrive in sequence on load so the eye follows the thread down. Capped
   at six: past that it is a queue forming, not a story. */
.chat-msg { animation: chat-rise .26s cubic-bezier(.22, 1, .36, 1) both; }
.chat-msg:nth-last-child(1) { animation-delay: .10s; }
.chat-msg:nth-last-child(2) { animation-delay: .08s; }
.chat-msg:nth-last-child(3) { animation-delay: .06s; }
.chat-msg:nth-last-child(4) { animation-delay: .04s; }
.chat-msg:nth-last-child(5) { animation-delay: .02s; }
.chat-msg:nth-last-child(n + 6) { animation-delay: 0s; }
/* Ten days at the counter: it recedes rather than shouting. */
.chat-msg.is-stale { transition: opacity .3s ease; }
.chat-card { animation: chat-rise .24s ease both; }
.chat-card:nth-child(2) { animation-delay: .03s; }
.chat-card:nth-child(3) { animation-delay: .06s; }
.chat-card:nth-child(4) { animation-delay: .09s; }
.chat-card:nth-child(n + 5) { animation-delay: .12s; }

/* Press states: the surface answers the thumb before the network does. */
.chat-card, .chat-chip, .chat-problem, .chat-cat, .chat-send,
.chat-icon-button, .chat-composer-extra {
    transition: transform .16s cubic-bezier(.22, 1, .36, 1),
                background-color .16s ease, opacity .16s ease;
}

/* A chip is used up once tapped; it leaves rather than lingering. */
.chat-chip { animation: chat-pop .2s ease both; }
.chat-chip:nth-child(2) { animation-delay: .04s; }
.chat-chip:nth-child(3) { animation-delay: .08s; }
.chat-chip.is-used {
    transform: scale(.7);
    opacity: 0;
    pointer-events: none;
}

/* Sending: the button acknowledges, and stays down while the post is in flight
   so a second tap is visibly not needed. */
.chat-send.is-sending { transform: scale(.9); opacity: .65; }

/* The sheets rise instead of appearing. Same curve as a message, because they
   are the same gesture: something arriving from below. */
.cat-sheet:not(.hidden) .cat-sheet-panel {
    animation: chat-sheet-up .24s cubic-bezier(.22, 1, .36, 1) both;
}
.cat-sheet:not(.hidden) .cat-sheet-backdrop {
    animation: chat-fade-down .2s ease both;
}

/* The app already answers prefers-reduced-motion globally, a few hundred
   lines up: every animation and transition collapses to .01ms. A second block
   here would be a second place to keep in step, and the first one to fall out
   of it. Nothing on this screen opts back in with !important, which is what
   makes the global rule sufficient — a test says so. */

/* ── Год помесячно, экран «Отчёты» ───────────────────────────────────────
   Одна линия капитала и двенадцать строк. Полосы внутри строки лежат
   горизонтально, а не вертикально: месяцев двенадцать, и вертикальные
   столбики на телефоне сжимаются в нечитаемую гребёнку. */

.needt-year-now { margin: 0; font-size: var(--text-caption); font-weight: 600; font-variant-numeric: tabular-nums; }
.needt-year-line {
    display: block;
    width: 100%;
    height: 92px;
    margin-top: .75rem;
    overflow: visible;
}
.needt-year-line polyline {
    fill: none;
    stroke: hsl(var(--foreground));
    stroke-width: 1.5;
    stroke-linejoin: round;
    stroke-linecap: round;
}
.needt-year-scale {
    display: flex;
    justify-content: space-between;
    margin-top: .375rem;
    font-size: 10px;
    color: hsl(var(--muted-foreground));
}

.needt-year-row {
    display: grid;
    grid-template-columns: 3.25rem minmax(0, 1fr) auto;
    align-items: center;
    gap: .625rem;
    min-height: 40px;
    padding: .25rem 0;
}
.needt-year-row.is-current .needt-year-month { color: hsl(var(--foreground)); font-weight: 600; }
.needt-year-month {
    font-size: 12px;
    color: hsl(var(--muted-foreground));
    font-variant-numeric: tabular-nums;
}
.needt-year-year { display: block; font-size: 10px; opacity: .7; }
.needt-year-bars { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.needt-year-bar { height: 6px; border-radius: var(--radius); min-width: 2px; }
.needt-year-bar--earned { background: hsl(var(--success-hsl)); }
.needt-year-bar--spent { background: hsl(var(--destructive)); }
.needt-year-kept { font-size: var(--text-body); font-weight: 600; font-family: var(--font-display); }
.needt-year-kept.is-negative { color: hsl(var(--destructive)); }

/* ── Квест недели ────────────────────────────────────────────────────────
   Счётчик, а не галочка: «12 из 20» держит усилие там, где «не сделано»
   только отталкивает. Полоса под строкой — не украшение, а то же число
   для бокового зрения. */
.needt-quest-bar i {
    display: block;
    height: 100%;
    background: hsl(var(--foreground));
}

/* Строка формы квеста: на телефоне в столбик, на десктопе в ряд. */
.needt-quest-row { display: grid; gap: .5rem; padding-bottom: .25rem; }
/* Своё display: grid перебивает [hidden] из preflight — прячем явно. */
.needt-quest-row[hidden] { display: none; }
@media (min-width: 640px) {
    .needt-quest-row { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
}
/* .ui-input тянется на всю ширину, поэтому ширину задаёт сетка, а не класс
   утилиты: w-20 на нём проигрывает и поле уезжает за край экрана. */
.needt-quest-count { display: grid; grid-template-columns: 4.5rem 6.5rem minmax(0, 1fr); gap: .5rem; align-items: end; }
/* Подписи у числа и цены: «5» и «80» без них не читались. Подпись только у
   первой строки — ниже колонки те же. */
.needt-quest-field { display: grid; gap: .25rem; min-width: 0; }
.needt-quest-field > span { font-size: 11px; font-weight: 600; color: hsl(var(--muted-foreground)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.needt-quest-row ~ .needt-quest-row .needt-quest-field > span { display: none; }
/* На телефоне три поля в ряд не влезают: способ подсчёта обрезался до
   «Counted by th». Число и цена — в ряд, способ — строкой ниже. */
@media (max-width: 639.98px) {
    .needt-quest-count { grid-template-columns: 4.5rem minmax(0, 1fr); }
    .needt-quest-count select { grid-column: 1 / -1; }
}

/* Вкладка «расход / приход» в листе добавления. Состояние выбора — заливка
   поверхностью, а не цветной текст: цвет на экране значит деньги, и красным
   здесь была бы подписана вкладка, а не сумма. */
.add-tab.is-on {
    background: hsl(var(--card));
    border-color: hsl(var(--border));
    color: hsl(var(--foreground));
}
.add-tab:not(.is-on) {
    background: transparent;
    border-color: transparent;
    color: hsl(var(--muted-foreground));
}

/* ── Внешний вид: выбор цвета и картинка на главной ──────────────────────*/
.needt-swatch.is-on { border-color: hsl(var(--foreground)); }

/* ── Разбор: выбор категории и догадка про возврат ───────────────────────*/
.sort-guess-copy strong {
    display: block;
    font-size: 13px;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sort-guess-copy small {
    display: block;
    margin-top: .0625rem;
    font-size: 11px;
    font-weight: 500;
    opacity: .75;
}

/* ── Десктоп: раскладка, а не увеличенные кнопки ─────────────────────────
   Прошлый заход поднял кегль и высоты — и на 27 дюймах получился телефон с
   крупным шрифтом: строка «Применить» растягивалась на 1400px, а половина
   экрана оставалась пустой. Размер — это не раскладка.

   Здесь три вещи: колонка контента получает потолок ширины, кластер
   действий перестаёт растягиваться, а боковое меню становится шире, потому
   что 220px на широком экране — это полоска, а не меню. */
@media (min-width: 1024px) {

    /* Экраны-очереди и списки: читаемая колонка вместо всей ширины.
       Строка длиной в монитор не читается — глаз теряет начало, пока
       доходит до конца. */

    /* Действия в строке разбора встают в ряд справа и держат свою ширину.
       Раньше `flex: 1` у «Применить» съедал всё свободное место. */

    /* Голова строки: имя слева, сумма справа, между ними воздух, а не
       растянутый элемент. */
}

/* Описание строки рельса: третья строка мелким, чтобы было видно, за что
   платил, не открывая операцию. */

/* На телефоне четыре контрола в ряд не помещаются в 375px: «Оставить как
   есть» уезжал за край, и решение, которое я же и добавил, было недоступно.
   Два ряда по два: сверху что меняем, снизу что делаем. */
@media (max-width: 767.98px) {
    .sort-merchant-actions > * { width: 100%; min-width: 0; }
    /* Строка «разовое или обычное» держала кнопки в одном ряду с именем и
       суммой: на 390px имя сжималось до одной буквы, дата вставала в
       столбик, сумма наезжала на неё. Кнопки уходят на свою строку. */
}

/* ── Сводка в поиске ──────────────────────────────────────────────────────
   Как карточка трекера трат: итог крупно, полгода пилюлями-столбиками,
   категории полосой из делений. */
.search-insight { padding: 1.25rem; }
.search-insight-total { margin: 0; font-size: 2.25rem; font-weight: 600; letter-spacing: -.03em; line-height: 1.1; }
.search-insight-sub { margin: .35rem 0 0; font-size: .8125rem; color: hsl(var(--muted-foreground)); }
.search-insight-months {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: .5rem;
    margin: 1.25rem 0 0;
    padding: 0;
    list-style: none;
}
.search-insight-months li { display: flex; flex-direction: column; align-items: center; gap: .3rem; min-width: 0; }
.search-insight-bar {
    display: flex;
    align-items: flex-end;
    width: 100%;
    max-width: 2.25rem;
    height: 4.5rem;
    border-radius: .5rem;
    background: hsl(var(--foreground) / .06);
    overflow: hidden;
}
.search-insight-bar i { display: block; width: 100%; border-radius: .5rem; background: hsl(var(--foreground) / .35); }
.search-insight-months li.is-current .search-insight-bar i { background: hsl(var(--foreground) / .85); }
.search-insight-months strong { font-size: .6875rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.search-insight-months small { font-size: .6875rem; color: hsl(var(--muted-foreground)); }
.search-insight-months li.is-current small { color: inherit; font-weight: 600; }
.search-insight-cats { display: flex; flex-direction: column; gap: .875rem; margin: 1.5rem 0 0; padding: 0; list-style: none; }
.search-insight-cats li { display: flex; align-items: center; gap: .75rem; }
.search-insight-cat-mark { flex: none; }
.search-insight-cat-body { flex: 1; min-width: 0; }
.search-insight-cat-line { display: flex; justify-content: space-between; gap: .75rem; font-size: .875rem; }
.search-insight-cat-line span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-insight-segs { display: grid; grid-template-columns: repeat(30, minmax(0, 1fr)); gap: 2px; margin-top: .4rem; }
.search-insight-segs i { display: block; height: .875rem; border-radius: 2px; background: hsl(var(--foreground) / .1); }

/* Экран «Отложено и впереди»: переход к разделу и запас сверху, чтобы
   заголовок раздела не уходил под край при переходе по якорю. */

/* Журнал изменений. */
/* «Отменить» после действия: полоса над нижней панелью, минуту. */
.undo-snack span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
