/*
 * Hello Budget Mini App.
 *
 * The palette, the type scale, the spacing grid, the radii, the shadows and the motion curves are
 * not here: they are in design-system.css, which is the group's design system copied in by
 * scripts/sync-design-system.sh rather than transcribed. This file is only what the app adds on
 * top of it — the screens, the keypad, the sheet, the lists.
 *
 * They used to be here, typed out by hand, under a comment that called them the design system.
 * They had already drifted from it: the ink scale stopped at seven of its twelve steps, the
 * shadows and the focus ring were missing altogether, and the motion tokens had names of their
 * own. A copied token is one that stops matching the day the system moves, and nothing fails when
 * it does.
 *
 * Where this file departs from the system it says so on the line, with the reason. There are two
 * such places and both are about a phone: the fields are 16px because anything smaller makes iOS
 * zoom the page when the field is focused, and the cards are padded at the system's small step
 * because a 390px screen is not a page on gpm.name.
 *
 * Every rule is written once. Half of this file used to be here twice — the keypad, the buttons,
 * the fields, the empty state — with the second copy quietly winning, so reading the first one
 * told you the wrong thing about what the screen looked like.
 */

:root {
  /* Rhythm.
     Three distances rather than one. Everything on every screen used to sit twelve or sixteen
     pixels from everything else, which is what makes a screen read as one undifferentiated column
     — the thing you notice as "it is all stuck together" without being able to point at a rule.
     What belongs to one another sits at --gap-tight, a block from the block under it at
     --gap-block, and a section from the next section at --gap-section. */
  --gap-tight: var(--space-2);
  --gap-block: var(--space-4);
  --gap-section: var(--space-7);

  /* the two fixed bars, named once: the screens are padded around them and half a dozen rules
     need to know how tall they are */
  --topbar-height: 44px;
  --tabbar-height: 64px;
  /* the add button floats over the tab bar rather than sitting in it, and the screens make room
     for both at the bottom */
  --fab-size: 56px;

  /* the width the app becomes on a screen wide enough to have room to spare — see the wide
     screens section near the end of this file. Named once because every position: fixed element
     that spans the window has to be pulled back in to the same figure, or the seam between it
     and the column of ordinary content shows */
  --column-max: 560px;

  /* the desktop side panel's own width, and the column its content lines up with — see the "wide
     screens: the standalone desktop app" section near the end of this file, which is the only
     place either is read */
  --side-width: 248px;
  --desk-column: 800px;

  /* telegram-web-app.js sets this on the root element to the height that is actually visible,
     which 100vh is not always: inside Telegram's own webview it can include area a toolbar or a
     keyboard is covering. The value here is what a plain browser gets, since nothing overrides it
     there. */
  --tg-viewport-stable-height: 100vh;

  /* how tall a key is, and the room between the writing and the keypad. They are tokens so that a
     short screen can take them in, below, without a second copy of the keypad's rules */
  --key-pad: var(--space-4);
  --keypad-lead: var(--space-5);
  --amount-pad: var(--space-4);
  --amount-size: 38px;

  /* List density.
     How tight the expenses and incomes rows, the subscription cards and the instalment cards
     sit — chosen in Settings, applied as data-density on the root element, and read only here:
     everything that draws one of those rows reaches for one of these tokens instead of a fixed
     space-* step. "Large" is what these defaults are, on purpose: it is what the app looked like
     before the setting existed, and it has to go on looking like that with nothing chosen. */
  --density-list-gap: var(--space-3);
  --density-expense-pad: var(--space-4);
  --density-expense-gap: var(--space-3);
  --density-card-pad: var(--space-5);
  --density-meta-gap: var(--space-2);
  --density-meta-line: 1.4;
  --density-meter-height: 6px;

  /* the Riepilogo's two donuts: six colours to cycle a category chart through, and one fixed
     colour for "Altro" and for nothing else. They are drawn from hues the design system already
     carries — the semantic ones it redefines for dark mode, plus two more of the same family —
     so the palette needs no dark-mode block of its own here. */
  --dash-c1: var(--accent);
  --dash-c2: var(--success);
  --dash-c3: var(--highlight);
  --dash-c4: var(--danger);
  --dash-c5: var(--cyan-500);
  --dash-c6: var(--synapsi-teal);
  --dash-other: var(--text-subtle);
}

/* "medium" is the new default, once the setting exists: noticeably tighter than the "large" above,
   without going as far as "compact" does. */
:root[data-density="medium"] {
  --density-list-gap: var(--space-2);
  --density-expense-pad: var(--space-3);
  --density-expense-gap: var(--space-2);
  --density-card-pad: var(--space-4);
  --density-meta-gap: var(--space-1);
  --density-meta-line: 1.3;
  --density-meter-height: 5px;
}

/* "compact": as tight as a row is still comfortable to tap. The expense row is the shortest of
   the three kinds this touches, and even here the day, the description and the category keep a
   good 40px between them and the row above. */
:root[data-density="compact"] {
  --density-list-gap: var(--space-2);
  --density-expense-pad: var(--space-2);
  --density-expense-gap: var(--space-2);
  --density-card-pad: var(--space-3);
  --density-meta-gap: var(--space-1);
  --density-meta-line: 1.2;
  --density-meter-height: 4px;
}

/* A short screen — most phones, once Telegram's own bar is off the top — takes some height back
   from the keypad, and a very short one from the figure too: five rows of keys at full size and the
   figure above them are most of 640 pixels, and what is left between them is where the categories
   and the date are chosen. */
@media (max-height: 820px) {
  :root {
    --key-pad: var(--space-3);
    --keypad-lead: var(--space-3);
  }
}
@media (max-height: 700px) {
  :root {
    --key-pad: var(--space-2);
    --amount-pad: var(--space-3);
    --amount-size: 32px;
  }
}

/* nothing on this page is meant to be dragged sideways except a handful of rows that scroll their
   own overflow-x on purpose — the chips, the day strip. A fixed amount, a long word without a
   break, or a row of narrow columns at 320px would otherwise give the whole page that drag too.
   This is the backstop: the page is clipped at the viewport's own edge, whatever a rule further
   down gets wrong. The body, further down, clips rather than hides: hidden would make it a
   scroll container of its own, one that never scrolls, and every sticky element inside — the day
   strip — would stick to it instead of to the window */
html { overflow-x: hidden; }

/* hidden means hidden, whatever the element's own rule says. The browser's own display:none for
   the attribute is the weakest rule on the page: any class that sets a display — .month's flex,
   .button's inline-flex — outranks it, and the element stays on screen with the attribute on it.
   That is how the charts screen came to show two rows of ‹ › at once, the month's and an empty
   one for the year (and both of them on "Sempre", which has neither): the double left-right that
   was reported against it. The same was true, less visibly, of every button the script hides —
   removing a budget that is not written yet, adding a subscription on the free plan. One rule
   here rather than a [hidden] line under every class that happens to set a display, which is the
   list that had already missed these */
[hidden] { display: none !important; }

body {
  /* the family, size, line height, colours and the reset come from the design system's base
     layer; what is left is the one thing that is about being a Mini App */
  padding-bottom: env(safe-area-inset-bottom);
  /* the other half of the sideways backstop, with the html rule above */
  overflow-x: clip;
}

/* the system's MetaLabel: mono, uppercase, wide tracking. Muted rather than subtle — at 12px on
   white, --text-subtle is 2.5:1, which is a section heading you have to look for */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ---- motion ----
   The keyframes everything borrows. They are declared once and used by name, so a screen, a row
   and a panel all enter the same way and the app reads as one thing rather than as a set of
   screens that each had their own idea. The durations and the curve are the system's. */

@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes bump {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.03); }
  100% { transform: scale(1); }
}

/* the placeholder rows while the answer is on its way: a block that sits still reads as a screen
   that has stopped, and the sweep is the difference between waiting and broken */
@keyframes shimmer {
  from { background-position: -160px 0; }
  to   { background-position: 240px 0; }
}

/* ---- screens ---- */

.screen {
  display: none;
  padding: var(--space-4);
  /* the top bar is fixed, so every screen starts below it */
  padding-top: calc(var(--space-5) + var(--topbar-height) + env(safe-area-inset-top));
  /* and the tab bar is fixed at the other end, with the add button floating above its right end:
     the last row of a list has to be able to scroll up clear of both, or its amount — which is on
     the right, where the button is — stays under it for good */
  padding-bottom: calc(var(--tabbar-height) + var(--fab-size) + var(--space-4) + var(--space-5));
}
.screen.is-active {
  display: block;
  animation: rise var(--dur) var(--ease-out) both;
}

/* ---- month header ---- */

.month {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
}
.month__label {
  font-size: var(--fs-h4);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-snug);
}
/* the label is what changed when an arrow was pressed, so it is what says so */
.month__label.is-changed { animation: fade var(--dur) var(--ease-out); }

.month__nav {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
  color: var(--text);
  font-size: 18px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--dur-fast) var(--ease-out), background var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}
.month__nav:active { background: var(--surface-2); border-color: var(--text-subtle); transform: scale(0.94); }
.month__nav:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* ---- total ---- */

.total {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
  padding: var(--space-5);
  margin-bottom: var(--gap-block);
}
.total__amount {
  font-family: var(--font-mono);
  font-size: var(--fs-h2);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
  margin-top: var(--space-1);
}

/* what came in and what is left of it, under what went out: two figures of the same size as each
   other and smaller than the one above, because the month is read as one number and then two */
.total__pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
/* the second of the two is fenced off from the first rather than merely spaced from it: two
   figures side by side at the same size read as one row of four words otherwise */
.total__pair > * + * { border-left: 1px solid var(--border); padding-left: var(--space-4); }
.total__side { min-width: 0; }
.total__small {
  font-family: var(--font-mono);
  font-size: var(--fs-h4);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-snug);
  margin-top: var(--space-1);
}
/* a difference — Differenza, on every screen that shows one — is green when more came in than went
   out and red when more went out; at exactly zero, or on an account that asked for no colour, it
   carries neither class and keeps the colour of the text. app.js paintBalance is what sets them */
.is-balance-positive { color: var(--success); }
.is-balance-negative { color: var(--danger); }

/* a figure that has just been recalculated says so, briefly. Without it a delete and a month
   change look the same as nothing happening */
.total__amount.is-changed, .total__small.is-changed { animation: bump var(--dur-slow) var(--ease-out); }

/* ---- lists ---- */

.list { display: flex; flex-direction: column; gap: var(--density-list-gap); }

/* the rows arrive one after the other rather than all at once. The delay is small and stops at
   the eighth: past that it is not a list appearing, it is a list being slow.

   Only on a list that is being filled for the first time, though — the app rebuilds a list from
   nothing every time one row of it changes, and re-staggering twenty rows because one amount was
   corrected is the flicker that reads as an app that cannot keep up. */
.list.is-fresh > * { animation: rise var(--dur) var(--ease-out) both; }
.list.is-fresh > *:nth-child(1) { animation-delay: 0ms; }
.list.is-fresh > *:nth-child(2) { animation-delay: 24ms; }
.list.is-fresh > *:nth-child(3) { animation-delay: 48ms; }
.list.is-fresh > *:nth-child(4) { animation-delay: 72ms; }
.list.is-fresh > *:nth-child(5) { animation-delay: 96ms; }
.list.is-fresh > *:nth-child(6) { animation-delay: 120ms; }
.list.is-fresh > *:nth-child(7) { animation-delay: 144ms; }
.list.is-fresh > *:nth-child(n+8) { animation-delay: 160ms; }

/* a bare list on a screen, with something under it: the "more" button on the admin users */
.list + .button { margin-top: var(--space-3); }

/* ---- day grouping: the header over a day's own rows, and the strip that jumps to one ---- */

/* scroll-margin-top is what a tapped chip in the strip below aims at: without it the header
   this scrolls to would land right under the fixed topbar, half covered by it. The figure this
   adds to --topbar-height is the strip's own height plus a little of the day header's own row —
   close enough that the header always clears both, whichever list drew it. */
.day-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding: 0 2px;
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
  scroll-margin-top: calc(var(--topbar-height) + env(safe-area-inset-top) + 56px);
}
/* the first header of a list sits right under the section head above it, not a gap of its own
   lower than the row it opens needs */
.list > .day-header:first-child { margin-top: 0; }

.day-header__total { font-family: var(--font-mono); white-space: nowrap; }

/* the jump strip: one chip per day the list carries a header for, in the list's own order. Sticky
   just under the fixed topbar — see the has-side override further down for the desktop bar's own
   height — while the list scrolls past it, and the only thing on either screen that ever scrolls
   sideways: overflow-x alone, no visible scrollbar, and overscroll-behavior-x so dragging past
   its own edge never hands the gesture to the page underneath it. */
.day-strip {
  position: sticky;
  top: calc(var(--topbar-height) + env(safe-area-inset-top));
  z-index: 5;
  display: flex;
  gap: var(--space-2);
  /* edge to edge across the screen's own padding: stuck under the top bar, a strip only as wide
     as the list leaves a column of rows sliding past on either side of it */
  margin: 0 calc(-1 * var(--space-4)) var(--space-3);
  padding: var(--space-2) var(--space-4) var(--space-3);
  scroll-padding-inline: var(--space-4);
  background: var(--bg);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.day-strip::-webkit-scrollbar { display: none; }
.day-strip[hidden] { display: none; }

.day-strip__chip {
  flex: none;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text-muted);
  font: inherit;
  font-size: var(--fs-caption);
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur) var(--ease-out), color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}
.day-strip__chip:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.day-strip__chip.is-selected {
  color: var(--text-on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

.expense {
  display: grid;
  /* the day used to be a column of its own here; grouped under a day header it would only repeat
     the header's own day, so it is the header that carries it now and the row keeps the two
     columns that are still its own */
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--density-expense-gap);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
  padding: var(--density-expense-pad);
  cursor: pointer;
  transition: border-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.expense:active { transform: scale(0.99); }
.expense.is-selected { border-color: var(--accent); box-shadow: var(--shadow-sm); }

.expense__body { min-width: 0; }
.expense__description {
  font-size: var(--fs-body-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.expense__category { font-size: var(--fs-caption); color: var(--text-muted); margin-top: 2px; }
.expense__amount {
  font-family: var(--font-mono);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-semibold);
  white-space: nowrap;
  text-align: right;
}
.expense__instalment { font-family: var(--font-mono); }

/* the shared-expense tag: a small pill under the category, rather than the muted dot it replaces,
   because the dot only said "shared" and not with which of two accounts. It sits on its own line
   under .expense__category and its label ellipsizes on its own — a long account name shrinks the
   pill, not the row it is in. */
.expense__tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  margin-top: var(--space-1);
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  border: 1px solid var(--accent-soft-border);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--fs-caption);
  line-height: 1.4;
}
.expense__tag-icon { flex: none; width: 12px; height: 12px; }
.expense__tag-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* an expense that got money back: the amount it was is struck through and the line under it says what
   came back and when. A row refunded entirely is dimmed too (it takes .is-idle), because it is still
   in the list for being there but it is not the month's money any more */
.expense__struck { text-decoration: line-through; }
.expense__refund {
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  color: var(--accent);
}

/* the line a month carries for money that came back in it: not an expense, so it is drawn apart from
   one (dashed, tinted) and what it says is negative. Tapping it opens the expense it is the refund of */
.expense--refund { border-style: dashed; border-color: var(--accent-soft-border); background: var(--accent-soft); }
.expense--refund .expense__amount { color: var(--accent); }

.empty {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  padding: var(--space-8) var(--space-4);
  text-align: center;
  color: var(--text-muted);
}

/* the date a run of search results falls under, spelled out with its weekday and year — the rows
   themselves keep their own plain day number, which on its own says nothing once results span
   more than one month at once. Read as a sentence, not a label: the platform capitalises only
   its first letter (see longDateName in i18n.js), so this is set in the app's own UI font at a
   sentence's own weight rather than the all-caps mono tags the rest of this screen uses for a
   short tag like a date or a total — a line this much longer reads better as a heading than as a
   label. The first one keeps its own margin rather than zeroing it against the top of the list,
   the way an :first-child rule here used to: the summary line above it needs the same breathing
   room the rest of them get from each other. */
.search__date {
  margin: var(--space-4) 0 var(--space-1);
  font-family: var(--font-sans);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
}

.skeleton {
  height: 72px;
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  background-image: linear-gradient(90deg, transparent, var(--border), transparent);
  background-repeat: no-repeat;
  background-size: 160px 100%;
  animation: shimmer 1100ms linear infinite;
}

/* ---- add card ---- */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: rgba(2, 6, 23, 0.55);
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out);
}
.sheet-backdrop.is-open { opacity: 1; }
.sheet-backdrop[hidden] { display: none; }

.sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  /* the fallback for a browser that never runs telegram-web-app.js; inside Telegram the custom
     property above is the one actually visible, which 100vh is not always */
  max-height: 92vh;
  max-height: calc(var(--tg-viewport-stable-height) * 0.92);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-top: 1px solid var(--border);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  box-shadow: var(--shadow-lg);
  transform: translateY(100%);
  transition: transform var(--dur-slow) var(--ease-out);
}
.sheet.is-open { transform: translateY(0); }
.sheet[hidden] { display: none; }

.sheet__grip {
  flex: none;
  width: 40px;
  height: 4px;
  margin: var(--space-3) auto var(--space-1);
  border-radius: var(--radius-pill);
  background: var(--border-strong);
}

/* the figure, pinned at the top of the card: it is what Salva writes down, so it is never what
   gives way to make room */
.sheet__head {
  flex: none;
  padding: var(--space-4) var(--space-4) 0;
}

/*
 * The part of the card that gives way.
 *
 * The keypad used to be the last thing in here, which meant Salva was wherever this scrolled to —
 * below the fold on a small phone with an edit note or the options row open above it. flex: 1
 * with min-height: 0 is what lets a flex item shrink under its own content and hand the room it
 * does not have any more to overflow-y, so neither the figure above it nor the keypad below it is
 * ever something you have to scroll to reach.
 */
.sheet__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* the scroll stops at the end of the card instead of carrying on into the screen behind it:
     scrolling the page under an open sheet is the jump that reads as the app losing its place */
  overscroll-behavior: contain;
  padding: 0 var(--space-4);
  -webkit-overflow-scrolling: touch;
}

/* the keypad and the two buttons under it, pinned below the part that scrolls: flex: none keeps
   this at its own content height, whatever .sheet__body above it gives up */
.sheet__footer {
  flex: none;
  padding: 0 var(--space-4) calc(var(--space-4) + env(safe-area-inset-bottom));
}

/*
 * Somebody writing on the card, with the phone's keyboard up — see sheetWriting in app.js.
 *
 * The keyboard is the keypad while it is there, so the footer steps aside and the room is the
 * field's. The card sits on the top edge of the keyboard rather than the bottom of the page and is
 * at most as tall as what the keyboard leaves: --sheet-lift and --sheet-room are the visual
 * viewport's, written on the card's own style by sheetFollowKeyboard, which wins over the values
 * here. These are what a browser with no visual viewport to read is left with: nothing to lift,
 * and the usual height.
 */
.sheet.is-writing {
  --sheet-lift: 0px;
  --sheet-room: var(--tg-viewport-stable-height);
  bottom: var(--sheet-lift);
  max-height: calc(var(--sheet-room) * 0.92);
}
.sheet.is-writing .sheet__footer { display: none; }
.sheet.is-writing .sheet__body { padding-bottom: var(--space-3); }

/* ---- amount ---- */

.amount-display {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  padding: var(--amount-pad);
  margin-bottom: var(--space-3);
  text-align: right;
}
/* the small word above the figure that says it is an income rather than an expense, and the
   figure's own colour along with it: the one thing on the card somebody glances at before saving */
.sheet__kind { margin-bottom: var(--space-1); }
.sheet__kind[hidden] { display: none; }
.amount-display--income { border-color: var(--success); }
.amount-display--income .sheet__kind,
.amount-display--income .amount-display__value { color: var(--success); }
.amount-display__pending {
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  color: var(--text-muted);
  min-height: 20px;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.amount-display__value {
  font-family: var(--font-mono);
  font-size: var(--amount-size);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-tight);
  overflow-x: auto;
  white-space: nowrap;
  /* the origin is the right edge, where the digits are: growing from the middle would move the
     number sideways every time it changed */
  transform-origin: right center;
}
/* the display answers the key that was pressed. Without it the only thing that says a tap landed
   is the digit itself, and a digit added to a long number is not something the eye catches */
.amount-display__value.is-bumped { animation: bump var(--dur-fast) var(--ease-out); }

/* ---- keypad ---- */

/* the keypad is the last thing on the card and the thing the card exists for: it gets the room
   left over, and a line above it that says where the writing stops and the arithmetic starts */
.keypad {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-2);
  margin-top: var(--keypad-lead);
  margin-bottom: var(--space-4);
  padding-top: var(--keypad-lead);
  border-top: 1px solid var(--border);
}

.key {
  font-family: var(--font-mono);
  font-size: 22px;
  font-weight: var(--fw-medium);
  padding: var(--key-pad) 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  /* a keypad gets hammered: without this every tap tries to select the label */
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.key:active { background: var(--surface-2); transform: scale(0.94); }
.key:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.key--op { color: var(--accent); }
.key--muted { color: var(--text-muted); }
.key--wide { grid-column: span 2; }

/* the operator that is waiting for its right hand side stays lit, so the display and the keypad
   agree about what the calculator is in the middle of */
.key--op.is-armed {
  background: var(--accent-soft);
  border-color: var(--accent-soft-border);
}

.key--save {
  color: var(--text-on-accent);
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: var(--shadow-xs);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: var(--fw-semibold);
}
.key--save:active { background: var(--accent-press); border-color: var(--accent-press); }
.key--save[disabled] { opacity: 0.45; transform: none; box-shadow: none; }

/* ---- chips ---- */

.chips {
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-1);
  /* the row scrolls sideways rather than wrapping: a wrapping row of thirty categories
     would push the keypad off the screen */
  scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.chips--wrap { flex-wrap: wrap; overflow-x: visible; }

/*
 * With a mouse or a trackpad the rows wrap instead. A row that scrolls sideways is a thumb's
 * gesture: a wheel scrolls up and down, the scrollbar is hidden above, and on the computer the
 * categories of the add card were a row nobody could get to the end of without knowing about
 * shift and the wheel. Every row is short enough to take it — the categories stop at eight and a
 * "Tutte…", the options and the days at a handful — so wrapping costs a line, not the keypad.
 */
@media (hover: hover) and (pointer: fine) {
  .chips { flex-wrap: wrap; overflow-x: visible; }
}

.chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-4);
  font-family: var(--font-sans);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--dur-fast) var(--ease-out), background var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}
.chip:active { transform: scale(0.94); }
.chip:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.chip.is-selected {
  color: var(--text-on-accent);
  background: var(--accent);
  border-color: var(--accent);
}
.chip--more { border-style: dashed; }
.chip--date, .chip--field { position: relative; overflow: hidden; }
/* the native picker is what opens on tap, so the input covers the chip and stays invisible */
.chip--date input, .chip--field input, .chip--field select {
  position: absolute;
  inset: 0;
  opacity: 0;
  border: none;
  padding: 0;
}
/* the count is typed on the chip itself: the field comes out of hiding while it has the focus, in
   the place of the word, wide enough for two digits and the placeholder, so what is typed is seen */
.chip--count:focus-within .chip__text { display: none; }
.chip--count input:focus {
  position: static;
  opacity: 1;
  width: 5ch;
  min-width: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: center;
  outline: none;
  -moz-appearance: textfield;
  appearance: textfield;
}
.chip--count input::placeholder { color: var(--text-muted); }
/* a plan that is typed rather than pressed: dashed like the other chip that asks for something */
.chip--field { border-style: dashed; }

/* where to start, on an account with nothing in it. It is a card and not a banner: it is a thing
   to read once and act on, not a state of the app */
.start__title {
  margin-bottom: var(--space-2);
  font-size: var(--fs-body);
  font-weight: var(--fw-semibold);
}
.start__steps {
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
  color: var(--text-muted);
  font-size: var(--fs-caption);
  line-height: var(--lh-body);
}
.start__step { display: flex; gap: var(--space-2); align-items: baseline; }
.start__step + .start__step { margin-top: var(--space-2); }
/* the tick is the whole of the state, so it keeps its width and the lines stay in a column */
.start__mark { flex: 0 0 auto; width: 1.25em; }
.start__step--done { color: var(--text); }

/* the line that says what the amount above is, when the amount above is not simply what was spent */
.sheet-note {
  margin-bottom: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--accent-soft-border);
  border-radius: var(--radius-md);
  background: var(--accent-soft);
  color: var(--text);
  font-size: var(--fs-caption);
  line-height: var(--lh-body);
  animation: rise var(--dur) var(--ease-out) both;
}
.sheet-note[hidden] { display: none; }
/* the card's own notice line, when what it says is that something went wrong */
.sheet-note--error { border-color: var(--danger); background: var(--danger-soft); }

/* the refund of an expense, in the card that corrects it: what came back so far, the action that
   changes it or takes it off, and the little form (all or a part, and the day) that writes it */
.refund { margin-top: var(--space-4); }
.refund__status {
  margin-bottom: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  color: var(--accent);
}
.refund__actions { display: flex; gap: var(--space-2); margin-top: var(--space-2); }
.refund__actions .button { width: auto; flex: 1 1 0; min-height: 40px; }
.refund__form { margin-top: var(--space-3); }

.budget-hint {
  min-height: 20px;
  margin-bottom: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  color: var(--text-muted);
  text-align: right;
  transition: color var(--dur) var(--ease-out);
}
.budget-hint--over { color: var(--danger); }

/* ---- fields ---- */

.field { margin-bottom: var(--space-4); }
.field__label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
}

.input, .select {
  width: 100%;
  min-height: 44px;
  padding: var(--space-2) var(--space-4);
  font-family: var(--font-sans);
  /* the system's field is 15px. This one is 16, and the reason is iOS: a field with type smaller
     than that makes Safari zoom the whole page when it is focused, and a Mini App that zooms is
     one somebody has to pinch back out of before they can carry on */
  font-size: 16px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  transition: border-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}
.input::placeholder { color: var(--text-subtle); }
.input:focus, .select:focus { outline: none; border-color: var(--accent); box-shadow: var(--shadow-focus); }
.input--description { margin-bottom: var(--space-3); }

/*
 * The suggestions above the description field. On a phone they are an ordinary row in the card,
 * directly over the field: the keypad is gone while the keyboard is up, so the card has the room,
 * and the row is scrolled into view together with the field (see bindSheetWriting).
 *
 * With a mouse they are laid over what is above the field instead of pushing it down. The card is
 * centred on a computer, so a row that took room of its own would move the whole card, keypad
 * included, every time the field is focused or a word typed; this way nothing moves. The row
 * grows upward from the field when it wraps.
 */
.description-field { position: relative; }
.chips--suggestions { margin-bottom: var(--space-2); }
.chips--suggestions[hidden] { display: none; }
.chip--suggestion { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
@media (hover: hover) and (pointer: fine) {
  .chips--suggestions {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    z-index: 2;
    margin-bottom: 0;
    padding: var(--space-2) 0;
    background: var(--surface);
  }
}

/* a control that sits on a row rather than owning one */
.select--inline { width: auto; padding: var(--space-2) var(--space-3); }

/* a line of explanation under a field, in the quiet colour of the label's caption */
.field__hint {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--fs-caption);
  color: var(--text-muted);
}
.field__hint:empty { display: none; }

/* two fields that belong together, such as the first and the last day of something. One under the
   other on a phone: two date fields side by side at 360px are too narrow for the system's own
   picker text, and a clipped date is a date nobody can check */
.field-pair[hidden] { display: none; }
@media (min-width: 480px) {
  .field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
}

/* a field that is a screen's own, with a list under it */
.screen > .input { margin-bottom: var(--space-3); }

/* ---- buttons ----
   The system's Button, in the two sizes this app needs. Large for the one action a card or a form
   exists for, medium for the ones that sit next to a heading or inside a fold — the system's small
   is 34px tall, which is under the 44 a thumb wants, and every button in here is for a thumb. */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 50px;
  padding: 0 var(--space-6);
  font-family: var(--font-sans);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
  border-radius: var(--radius-lg);
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--text-on-accent);
  box-shadow: var(--shadow-xs);
  cursor: pointer;
  text-align: center;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur) var(--ease-out), color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out), transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur) var(--ease-out);
}
/* the system's press: half a pixel down and a fraction smaller. The app used to take two per cent
   off every button, which on a full width one is the whole row jumping */
.button:active { background: var(--accent-press); transform: translateY(0.5px) scale(0.992); }
.button:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.button[disabled] { opacity: 0.45; cursor: default; transform: none; box-shadow: none; }

/* the system's secondary: a real surface with a border, not a hole in the page */
.button--ghost {
  background: var(--surface);
  color: var(--text);
  border-color: var(--border-strong);
}
.button--ghost:active { background: var(--surface-2); border-color: var(--text-subtle); }

.button--danger {
  background: var(--danger);
  border-color: var(--danger);
  color: #FFFFFF;
}

.button--small {
  min-height: 42px;
  padding: 0 var(--space-5);
  font-size: var(--fs-body-sm);
  border-radius: var(--radius-md);
}

/* a label that behaves like a button, because a file input cannot be styled and can be hidden
   behind one that can */
.import-button { cursor: pointer; }

/* ---- tab bar ---- */

.tabs {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  align-items: center;
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom);
}
/* the hidden attribute only sets display:none at user agent priority, which display:grid
   above wins against: without this the bar stays on screen on the landing */
.tabs[hidden] { display: none; }

.tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-height: var(--tabbar-height);
  padding: var(--space-2) 0 var(--space-3);
  background: none;
  border: none;
  border-top: 2px solid transparent;
  color: var(--text-muted);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}
.tab__icon {
  width: 22px;
  height: 22px;
  /* the icon takes the colour of the tab, so the active state needs saying only once */
  color: inherit;
  transition: transform var(--dur) var(--ease-out);
}
.tab:active .tab__icon { transform: scale(0.88); }
.tab.is-active .tab__icon { transform: translateY(-1px); }
.tab:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

.tab__label {
  display: block;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
}
.tab.is-active { color: var(--accent); border-top-color: var(--accent); }

/* five labels: the mono caps-and-tracking style the bar used while it carried four of them does
   not fit five on a phone — "Riepilogo" and "Investimenti", first and last, ran past the edge of
   the screen they were on. Below 480px the bar reads in the plain UI font instead, sentence case,
   no tracking, at the same 10px the caps were. The 9px below 375px is from when the add button
   took a sixth of the bar as well; it is left as the floor, because a label smaller than that
   stops being one anybody reads. */
@media (max-width: 480px) {
  .tab__label {
    font-family: var(--font-sans);
    font-weight: var(--fw-medium);
    letter-spacing: 0;
    text-transform: none;
    white-space: nowrap;
  }
}
@media (max-width: 374px) {
  .tab__label { font-size: 9px; }
}

/* the add button is the only filled thing on the screen, because it is the one action the app
   exists for. It floats over the right end of the bar instead of taking a cell in it: in the bar it
   could only be in the middle while the tabs came in an even number, and five do not. On the
   right it is also where a thumb already is. */
.fab {
  position: fixed;
  right: calc(var(--space-4) + env(safe-area-inset-right));
  bottom: calc(var(--tabbar-height) + env(safe-area-inset-bottom) + var(--space-4));
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--fab-size);
  height: var(--fab-size);
  padding: 0;
  border-radius: var(--radius-pill);
  border: 1px solid var(--accent);
  background: var(--accent);
  box-shadow: var(--shadow-lg);
  color: var(--text-on-accent);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur) var(--ease-out);
}
.fab__icon {
  width: 24px;
  height: 24px;
  transition: transform var(--dur) var(--ease-out);
}
.fab:active { background: var(--accent-press); }
.fab:active .fab__icon { transform: scale(0.86) rotate(90deg); }
.fab:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* ---- cards, rows, sections ---- */

/* a heading opens a section, and a section is the widest gap on the screen. The top margin is on
   the heading itself rather than on the thing above it: whatever comes next, the distance to the
   heading before it is the same one everywhere */
.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin: var(--gap-section) 0 var(--space-3);
}
.screen > .section-head:first-child { margin-top: 0; }
.section-head--spaced { margin-top: var(--gap-section); }
/* the button next to a heading is a button, not a column: without this the flex row stretches it
   to whatever is left of the width, and "Aggiungi" ends up the size of a save button */
.section-head .button { flex: none; width: auto; }
.section-head__count { font-family: var(--font-mono); font-size: var(--fs-mono-label); color: var(--text-muted); }

/* the system's Card at its small padding: twenty rather than the twenty-four of a page, because
   a 390 pixel screen has 350 of width to spend and a card that keeps 48 of them for its own
   margins is a card with nothing in it */
.card {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
  padding: var(--space-5);
}

/* a card that opens something is a button, so a keyboard reaches it — and a button brings its
   own font, its own width and its text in the middle, none of which a card has */
button.card {
  display: block;
  width: 100%;
  font: inherit;
  color: inherit;
  text-align: inherit;
}

/* a card of rows: they are separated by a line rather than by nothing at all. Three of them with
   eight pixels between the words was the wall of text on the profile screen */
.row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
}
/* a button beside a label is as wide as its word, the same as in a section head: .button is 100%
   wide everywhere else, and in a row that squeezed the label into a column of broken lines while
   an investment alert's "Togli" took the rest */
.row > .button { flex: none; width: auto; }
.card > .row + .row { border-top: 1px solid var(--border); }
.card > .row:first-child { padding-top: 0; }
.card > .row:last-child { padding-bottom: 0; }
.row__label { font-size: var(--fs-body-sm); }
.row__value { font-family: var(--font-mono); font-size: var(--fs-body-sm); font-weight: var(--fw-semibold); }
.row__meta { font-size: var(--fs-caption); color: var(--text-muted); }
/* the note under a row belongs to it, but not so closely that it touches it */
.card > * + .row__meta { margin-top: var(--space-2); }
/* two actions one under the other, on a card that offers both */
.card > .button + .button, .card > .row__meta + .button { margin-top: var(--space-3); }

/* two buttons to a row, for the ones that come in families: four exports one under the other are
   four full width buttons, which reads as four decisions rather than as one list */
.grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
  margin: var(--space-2) 0 var(--space-4);
}

.form__actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
}

/* ---- rows that can be acted on ---- */

/* three buttons where there were two: cancel, correct, delete */
.row-actions {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
  margin-top: var(--space-2);
  animation: rise var(--dur-fast) var(--ease-out) both;
}

/* the actions of a purchase in instalments that do not fit the three columns: ending it early, and
   the little form that asks how and from when */
/* the second row of a strip: the refund and ending a plan early, side by side and as wide as the strip */
.row-actions__more {
  grid-column: 1 / -1;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--space-2);
}
.row-actions__more .button { white-space: normal; }

/* the card opened by a strip's "Rimborso": the figure, the refund and Annulla. What edits or saves
   the expense is out of the way, and so is the toggle of the form, which is already open */
.sheet.is-refund #edit-note, .sheet.is-refund #budget-hint, .sheet.is-refund #options,
.sheet.is-refund #categories, .sheet.is-refund #dates, .sheet.is-refund #description-field,
.sheet.is-refund #keypad, .sheet.is-refund #refund-open { display: none; }
.row-actions__wide, .instalment-end { grid-column: 1 / -1; }
.instalment-end { display: grid; gap: var(--space-2); }
.instalment-end .chips { margin-bottom: 0; }

/* two buttons where there is no correcting to offer: an investment movement is written and taken
   away, never changed in place */
.row-actions--pair { grid-template-columns: repeat(2, 1fr); }

/* ---- budgets ---- */

.budget { display: block; cursor: pointer; }
.budget__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.budget__name { font-size: var(--fs-body-sm); font-weight: var(--fw-semibold); }
.budget__amounts { font-family: var(--font-mono); font-size: var(--fs-caption); color: var(--text-muted); }
/* which days the figures are about, for a budget whose period is not the month on the header */
.budget__window {
  margin: calc(-1 * var(--space-2)) 0 var(--space-3);
  font-size: var(--fs-caption);
  color: var(--text-muted);
}

.meter {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  overflow: hidden;
}
/* the bar grows into place from nothing: a bar that is simply there says how full the budget is,
   a bar that fills says how full it got.

   It grows by being scaled, not by being widened. A width that changes is a layout the browser
   works out again on every frame of the animation, and a screen with twenty of them on it — the
   charts — is where that stops being free and starts being the stutter you can see. */
.meter__fill {
  height: 100%;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  border-radius: var(--radius-pill);
  background: var(--accent);
  transition: transform var(--dur-slow) var(--ease-out), background var(--dur) var(--ease-out);
}
/* over budget is the one thing on this screen worth a colour of its own */
.meter__fill--over { background: var(--danger); }

/* ---- subscriptions and incomes ---- */

.subscription {
  /* the size of a card, densified: the base .card rule sets it for every card on the app, and a
     subscription, income or instalment card is the only kind the setting touches */
  padding: var(--density-card-pad);
  cursor: pointer;
  transition: border-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.subscription:active { transform: scale(0.99); }
.subscription.is-selected { border-color: var(--accent); box-shadow: var(--shadow-sm); }
.subscription > * + .row__meta { margin-top: var(--density-meta-gap); }
.subscription .row__meta { line-height: var(--density-meta-line); }
.subscription .meter { height: var(--density-meter-height); }

/* something that takes or brings nothing in the month on the screen. It is here because it exists,
   not because it is part of the figure above: still readable, still tappable, and no longer one of
   the rows the total is made of */
.is-idle { opacity: 0.55; }

/* a subscription or a purchase in instalments that has already ended, on the "Tutti" chip: the two
   groups are told apart at a glance rather than only by reading the date on the meta line */
.is-ended { opacity: 0.55; }

.subscription-form { margin-bottom: var(--space-4); }
.subscription-form .field:last-of-type { margin-bottom: var(--space-4); }

/* ---- folds ----
   A section that is closed until it is asked for. The profile carried thirteen of these open at
   once; the guide chapters were the only thing already folded, and they were folded by a rule that
   another rule further down the file was overriding into a two column grid. */

.folds { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--gap-section); }
/* a fold is spaced by the stack it is in, so it carries no margin of its own: the two together
   were what put a different distance between two folds than between a fold and anything else */
.fold {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
}
/* one on its own, on a screen rather than in a stack of them */
.screen > .fold { margin-bottom: var(--gap-block); }

.fold__title {
  list-style: none;
  cursor: pointer;
  min-height: 52px;
  padding: var(--space-3) var(--space-5);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  -webkit-tap-highlight-color: transparent;
}
.fold__title::-webkit-details-marker { display: none; }
.fold__title:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: var(--radius-lg); }

/* the name takes the room, so whatever comes after it — a figure, the chevron — sits at the far
   edge whether or not there is a figure. Pushing the chevron with a margin instead put it against
   the name on every section that had no figure to show */
.fold__title > span:first-child { flex: 1; }

/* the figure a closed section would otherwise have to be opened to see */
.fold__value {
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  color: var(--text-muted);
}

.fold__title::after {
  content: "";
  width: 8px;
  height: 8px;
  border-right: var(--border-thick) solid var(--text-muted);
  border-bottom: var(--border-thick) solid var(--text-muted);
  transform: rotate(45deg);
  transition: transform var(--dur) var(--ease-out);
  flex: none;
}
.fold[open] > .fold__title::after { transform: rotate(-135deg); }
/* what is open is fenced from its own title, so the eye can tell the heading from the contents */
.fold[open] > .fold__title { border-bottom: 1px solid var(--border); }

.fold__body {
  padding: var(--space-5);
}
.fold__body > * + * { margin-top: var(--space-3); }

/* the subscriptions fold's own two groups, Abbonamenti and Rate: each a small heading with its
   own subtotal, the same figure style the fold's title carries, over its own list */
.fold__group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
}

/* the fold opens by growing rather than by appearing. A section that snaps to its full height is
   the single most abrupt thing this app did, and the profile is nothing but sections. The height
   is set from script for the length of the animation and given back to the browser after it */
.fold__body.is-folding {
  overflow: hidden;
  /* the padding animates with the height: a border-box of height nothing is still as tall as its
     own padding, so the height alone would stop the fold short of closed */
  transition: height var(--dur) var(--ease-out), padding var(--dur) var(--ease-out);
}

/* a chapter of the guide: the same fold, with the text of the chapter inside it */
.fold__text {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body);
  color: var(--text-muted);
  white-space: pre-line;
}

/* an address or a command to copy: the text on the left, wrapped where it has to, the button beside it */
.copy-field {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.copy-field__value {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-body-sm);
  color: var(--text);
  overflow-wrap: anywhere;
}

/* ---- messages ---- */

.notice {
  position: sticky;
  top: 0;
  z-index: 10;
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  background: var(--surface);
  box-shadow: var(--shadow-md);
  padding: var(--space-4);
  margin: var(--space-3) var(--space-4) 0;
  border-radius: var(--radius-md);
  font-size: var(--fs-body-sm);
  animation: rise var(--dur) var(--ease-out) both;
}
.notice--error { border-left-color: var(--danger); }
.notice[hidden] { display: none; }

/* ---- charts ---- */

.chart-card { margin-bottom: var(--gap-block); }
.chart-card .eyebrow { margin-bottom: var(--space-4); }

/* ---- riepilogo: tiles and donuts ---- */

/* the five things the second block of the Riepilogo counts: a button each, two to a row on a
   phone, so the screen reads as a grid of small facts rather than a column of five full-width
   ones the thumb has to scroll past to reach the charts under them */
.tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
  margin-bottom: var(--gap-block);
}
.tile {
  display: block;
  text-align: left;
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xs);
  padding: var(--space-4);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out), border-color var(--dur) var(--ease-out);
}
.tile:active { transform: scale(0.99); }
.tile:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* the count on top, small and muted — it is the label, not the figure — and the total under it
   in the same bold mono every other amount in the app is set in */
.tile__label {
  margin-bottom: var(--space-1);
  font-size: var(--fs-caption);
  color: var(--text-muted);
}
.tile__amount {
  font-family: var(--font-mono);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-semibold);
}

/* five tiles, two columns: the fifth has nothing to sit beside, so it takes the row to itself
   rather than leaving a hole — on the wide row below this becomes one of five equals again */
.tile:nth-child(5) { grid-column: 1 / -1; }

/* the donut itself: drawn as concentric stroked circles rather than pie wedges, which is what
   lets each slice grow from a plain stroke-dasharray instead of hand built arc paths — see
   renderDonut in app.js. Capped at a comfortable reading size rather than filling the card, since
   a donut that is mostly empty middle says nothing extra for being bigger. */
.donut {
  display: block;
  width: 160px;
  height: 160px;
  margin: 0 auto var(--space-4);
}
.donut circle { transition: stroke-dashoffset var(--dur-slow) var(--ease-out); }

.donut__legend { display: flex; flex-direction: column; gap: var(--space-2); }
.donut__legend-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-caption);
}
.donut__swatch {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: var(--radius-sm);
}
.donut__legend-label {
  flex: 1;
  min-width: 0;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.donut__legend-value {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  color: var(--text-muted);
}

/* the period's own card of figures sits between two chart-cards, which already carry the gap
   below themselves — this one needed the same gap and had none, being a plain .card rather than
   one of those */
#dashboard-pro-content > .card:not(.chart-card) { margin-bottom: var(--gap-block); }
/* the months a period spans, under the title that names the choice — its own line now rather
   than sharing one with the title, which is what was repeating the year. The section-head above
   it already carries the gap between the title and this line; this only adds the gap to what
   comes after it */
#dashboard-period-range { margin-bottom: var(--space-4); }

.bar {
  display: grid;
  /* the middle column a minmax rather than a bare 1fr: a bare 1fr's own floor is its content's,
     which for the track underneath it is nearly nothing anyway, but naming the floor rather than
     leaving it implicit is what keeps a very large amount in the last column from ever being the
     thing that pushes this row, and the page under it, wider than the screen */
  grid-template-columns: 88px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.bar:last-child { margin-bottom: 0; }
.bar__label {
  font-size: var(--fs-caption);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bar__track {
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  overflow: hidden;
}
/* scaled rather than widened, for the reason the budget meter is */
.bar__fill {
  height: 100%;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  border-radius: var(--radius-pill);
  background: var(--accent);
  transition: transform var(--dur-slow) var(--ease-out);
}
.bar__value {
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  white-space: nowrap;
}
/* A budget that is over is over on the bar too: colour alone is not something everybody sees, so
   the value is also set in bold. */
.bar--over .bar__fill { background: var(--danger); }
.bar--over .bar__value { color: var(--danger); font-weight: var(--fw-semibold); }

/* ---- charts: the day/month/year series, two columns per point ----
   Expenses and incomes reuse the same two colours the rest of the app already gives them: danger
   for what leaves, success for what arrives — see .amount-display--income and .budget-hint--over.
   The columns are a flex row and not a grid, so a narrow phone simply narrows every column by the
   same amount instead of the last one overflowing past the edge. */

.series__legend {
  display: flex;
  gap: var(--space-4);
  margin-bottom: var(--space-3);
  font-size: var(--fs-caption);
  color: var(--text-muted);
}
.series__legend-item { display: inline-flex; align-items: center; gap: var(--space-1); }
.series__dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.series__dot--expense { background: var(--danger); }
.series__dot--income { background: var(--success); }

/* the only axis label this chart carries: the tallest column is worth this much, and the baseline
   under the columns is worth nothing */
.series__max {
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  color: var(--text-muted);
  margin-bottom: var(--space-2);
}

.series__track {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 120px;
  border-bottom: 1px solid var(--border-strong);
}

.series__col {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-end;
  height: 100%;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
  -webkit-tap-highlight-color: transparent;
}
.series__col:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

.series__bars {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 1px;
  height: 100%;
  width: 100%;
}
/* scaled by height rather than a fixed width, so 28 to 31 of these still fit a 360px phone without
   the row scrolling sideways — the same reason the category bars are scaled and not widened */
.series__bar {
  flex: 1 1 0;
  min-width: 1px;
  max-width: 10px;
  border-radius: 1px 1px 0 0;
  transition: opacity var(--dur) var(--ease-out);
}
.series__bar--expense { background: var(--danger); opacity: 0.55; }
.series__bar--income { background: var(--success); opacity: 0.55; }
.series__col.is-selected .series__bar--expense,
.series__col.is-selected .series__bar--income { opacity: 1; }

.series__tick {
  margin-top: var(--space-1);
  font-size: 10px;
  color: var(--text-subtle);
  text-align: center;
  white-space: nowrap;
}
/* the first and last columns are against the track's own edge, clipped by its overflow-x: hidden
   — a label centred over either of them runs half off that edge rather than over the column, so
   these two grow inward instead, the direction the track still has room in */
.series__tick--start { text-align: left; }
.series__tick--end { text-align: right; }

.series__selected {
  margin-top: var(--space-3);
  font-size: var(--fs-body-sm);
  color: var(--text);
}
.series__selected[hidden] { display: none; }

.series__totals {
  margin-top: var(--space-2);
  font-size: var(--fs-body-sm);
  color: var(--text-muted);
}

/* ---- users, categories: a row with something to press ---- */

.user {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
  padding: var(--space-4);
}
.user__name { font-size: var(--fs-body-sm); }
.user__meta { font-family: var(--font-mono); font-size: var(--fs-mono-label); color: var(--text-muted); }

/* ---- category picker ----
   The row shows the few that are actually used and sends the rest to a screen of its own.
   A search field inside the add card is a row the keypad loses, and tapping it brings up
   a keyboard that covers the card it is in. */

.picker {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  padding: var(--space-4);
  padding-top: calc(var(--space-4) + env(safe-area-inset-top));
  padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
  gap: var(--space-4);
  animation: rise var(--dur) var(--ease-out) both;
}
.picker[hidden] { display: none; }
.picker__head {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--space-3);
}
/* the field on this head is the head, so it keeps no margin of its own */
.picker__head .input { margin-bottom: 0; }
.picker__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.picker__row {
  text-align: left;
  font: inherit;
  font-size: 16px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out), border-color var(--dur) var(--ease-out);
}
.picker__row:active { transform: scale(0.99); }
.picker__row:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.picker__row.is-selected { border-color: var(--accent); }

/* the row that creates the category just searched for, when the search names none already there:
   dashed like the other controls that ask for something rather than choose it */
.picker__add {
  text-align: left;
  font: inherit;
  font-size: 16px;
  color: var(--text);
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out), border-color var(--dur) var(--ease-out);
}
.picker__add:active { transform: scale(0.99); }
.picker__add:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* ---- the emoji of a category ----
   32 common ones on a grid, and a small box to paste any other: a category is an emoji and a name,
   and the emoji is not optional. The chosen one is marked the way a chosen chip is. */
.emoji-picker { display: grid; gap: var(--space-2); margin-bottom: var(--space-3); }
.user .emoji-picker { grid-column: 1 / -1; margin-bottom: 0; }
.emoji-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--space-1); }
.emoji-grid__item {
  font: inherit;
  font-size: 20px;
  line-height: 1;
  padding: var(--space-2) 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
}
.emoji-grid__item:active { transform: scale(0.94); }
.emoji-grid__item:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.emoji-grid__item.is-selected { background: var(--accent-soft); border-color: var(--accent); }
.emoji-picker__input { max-width: 14rem; }
.emoji-picker__hint { font-size: var(--fs-caption); color: var(--danger); }
.picker__new { display: grid; gap: var(--space-3); padding: var(--space-3) 0; }

/* ---- writing a budget ----
   The categories a budget covers wrap rather than scroll: here the list is being read, not
   walked past, and one hidden off the right is one nobody ticks. */
.budget-form { margin-bottom: var(--space-4); }
.budget-form .button--danger { margin-top: var(--space-3); }

/* ---- stats ----
   Figures first, caption under: the screen is read by scanning the numbers down the
   grid, and a label above each one would put four words between every two of them. */
.stats__title {
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
  margin-bottom: var(--space-3);
}
.stats__title + .stats { margin-bottom: var(--space-6); }
.stats:last-child { margin-bottom: 0; }

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: var(--space-3);
}
.stat {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  animation: fade var(--dur) var(--ease-out) both;
}
.stat__value { font-family: var(--font-mono); font-size: 20px; line-height: var(--lh-snug); }
.stat__label { font-size: var(--fs-mono-label); color: var(--text-muted); margin-top: var(--space-1); }

/* ---- pro ----
   What the two plans are, side by side. A list of what is missing is not a reason to pay: the
   free column says what somebody already has, and the numbers in it are the server's, asked for
   rather than written here, so a limit changed in the configuration is not a lie on this screen. */

.plans {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  margin: var(--space-3) 0 var(--space-4);
}
.plan {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.plan--pro { border-color: var(--accent-soft-border); background: var(--accent-soft); }
.plan__name {
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-subtle);
  margin-bottom: var(--space-3);
}
.plan--pro .plan__name { color: var(--accent); }
.plan__line {
  font-size: var(--fs-caption);
  line-height: var(--lh-body);
  color: var(--text-muted);
}
.plan__line + .plan__line { margin-top: var(--space-2); }

/* ---- landing ----
   What somebody who reached the page outside Telegram sees. The page has to be
   publicly reachable for the client to load it, so this is a real screen, not a
   fallback: it says why nothing works here and where to go instead. */

.landing {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  min-height: 100dvh;
  padding: var(--space-6);
}
.landing[hidden] { display: none; }

.landing__card {
  width: 100%;
  max-width: 420px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  padding: var(--space-7);
  text-align: center;
  animation: rise var(--dur-slow) var(--ease-out) both;
}
.landing__card--wide { max-width: 460px; text-align: left; }

.landing__title {
  margin: var(--space-4) 0 var(--space-3);
  font-size: var(--fs-h2);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight);
  line-height: var(--lh-heading);
}

.landing__text {
  margin: 0 0 var(--space-6);
  color: var(--text-muted);
  font-size: var(--fs-body-sm);
}

/* the consent screen's two buttons, one under the other, and the lines about who is asking */
.landing__card .button + .button { margin-top: var(--space-3); }
#connect-body .row__meta { margin: calc(var(--space-3) * -1) 0 var(--space-4); }

.landing__button { text-decoration: none; }
/* a second button — "open in the browser" — only ever follows the first: see landing-open-web */
.landing__button + .landing__button { margin-top: var(--space-3); }

.landing__meta {
  margin: var(--space-4) 0 0;
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  color: var(--text-subtle);
}

/* ---- web sign in ----
   The standalone web app's own front door: the same card the landing uses, with a way in rather
   than only a way out. Its fields are left aligned — a form centred like the sentences above it
   reads worse the moment there is more than one line in it. */

.login__error {
  margin-bottom: var(--space-4);
  border: 1px solid var(--border);
  border-left: 3px solid var(--danger);
  background: var(--surface);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--fs-body-sm);
  text-align: left;
}
.login__error[hidden] { display: none; }

.login__status {
  font-size: var(--fs-body-sm);
  color: var(--text-muted);
}
.login__status[hidden] { display: none; }

#login-email-form,
#login-email-sent {
  text-align: left;
}
#login-email-form[hidden],
#login-email-sent[hidden] { display: none; }

/* the field here is a <label>, which is inline: its margin never reached the button under it, and
   the address sat on top of "Mandami il link" */
#login-email-form .field { display: block; margin-bottom: var(--space-3); }

#login-email-sent .landing__text { margin-bottom: var(--space-3); }

.login__divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-6) 0 var(--space-4);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  text-transform: uppercase;
  letter-spacing: var(--ls-label);
}
.login__divider::before,
.login__divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}
.login__divider[hidden] { display: none; }

#login-telegram {
  display: flex;
  justify-content: center;
  min-height: 40px;
}

/* the ghost button already is what Google's own branding guidelines ask for — a neutral surface,
   never pure white against a dark page — so the "G" and its own margin are all this adds */
.button--google {
  margin-bottom: var(--space-4);
}
.button--google svg { flex-shrink: 0; }
.button--google[hidden] { display: none; }

/* ---- welcome ----
   One line per tab. These carried the class the guide chapters carry, further down the same file,
   which meant the chapters were being drawn as this: a summary squeezed into a 76px column with
   its chapter next to it. Two different things now have two different names. */

.map {
  margin: 0 0 var(--space-6);
  padding: 0;
  list-style: none;
}
.map__item {
  display: grid;
  /* wide enough for the longest of them, INVESTIMENTI, which ran into its own line at 76px */
  grid-template-columns: 104px 1fr;
  gap: var(--space-3);
  align-items: baseline;
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}
.map__item:first-child { border-top: none; }
.map__key {
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--accent);
}
.map__text { font-size: var(--fs-body-sm); color: var(--text-muted); }

/* ---- shared accounts ----
   Opening one, and inviting somebody into it. The form is built with the card and hidden rather
   than created on the press: half a handle typed and then lost to a redraw is how somebody gives
   up on a feature. */

.shared-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.shared-form[hidden] { display: none; }

/* ---- top bar ---- */

/* Two things live up here rather than in the tab bar: the profile and, for whoever runs the
   instance, the admin section. Neither is something you tap while writing down what you spent,
   and the bar below is a grid of five — a sixth button fell onto a second row. */

.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 25;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  height: calc(var(--topbar-height) + env(safe-area-inset-top));
  padding: env(safe-area-inset-top) var(--space-4) 0;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.topbar[hidden] { display: none; }

.topbar__title {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
}

/* the desktop's own page title, in .topbar__title's place: see showScreen and useLanguage, which
   fill it, and the wide-screens section near the end of this file, the only place that shows it */
.topbar__screen { display: none; }

/* the search button sits next to the overflow one rather than between the title and it, so the
   two share the row's own trailing edge instead of the title's space:space-between spreading a
   third item out across the middle of the bar */
.topbar__actions { display: flex; align-items: center; gap: var(--space-1); }

.topbar__more {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur) var(--ease-out), color var(--dur) var(--ease-out);
}
/* only the last of the two buttons is pulled back to the bar's own edge */
.topbar__actions .topbar__more:last-child { margin-right: calc(var(--space-2) * -1); }

.topbar__more svg { width: 20px; height: 20px; }
.topbar__more[aria-expanded="true"] { background: var(--surface-2); color: var(--text); }
.topbar__more:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

.overflow-menu {
  position: fixed;
  top: calc(var(--topbar-height) + env(safe-area-inset-top) + var(--space-1));
  right: var(--space-3);
  z-index: 26;
  display: flex;
  flex-direction: column;
  min-width: 180px;
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  transform-origin: top right;
  animation: rise var(--dur-fast) var(--ease-out) both;
}

.overflow-menu[hidden] { display: none; }

.overflow-menu__item {
  min-height: 44px;
  padding: 0 var(--space-3);
  display: flex;
  align-items: center;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-body-sm);
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}

.overflow-menu__item[hidden] { display: none; }
.overflow-menu__item:active { background: var(--surface-2); }
.overflow-menu__item:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* ---- wide screens: the standalone web app ----
   Inside Telegram the Mini App already sits in a narrow frame of the client's own; opened as
   hello-budget-app.gpm.name in an ordinary browser there is no such frame, and a list of expenses
   stretched across a 1440px window reads nothing like the app it is supposed to be.

   Above 720px the page becomes a centred column the width a phone would have been. Everything in
   the normal flow — every screen, the landing, the login screen — is bounded by the body's own
   max-width below; what is not, because it is position: fixed and spans left:0 to right:0 or
   inset:0, is bounded here to the same figure, so the seam between the two never shows. The
   sheet's backdrop is the one exception, left full width on purpose: it dims the whole window
   behind a card that sits in the column, which is the ordinary shape of a modal. */

@media (min-width: 720px) {
  body {
    max-width: var(--column-max);
    margin: 0 auto;
  }

  .topbar,
  .tabs,
  .sheet,
  .picker {
    max-width: var(--column-max);
    margin-left: auto;
    margin-right: auto;
  }

  /* the menu hangs off the topbar's own button, which the rule above has already pulled in to
     the column: it is measured from the same edge, not from the window's */
  .overflow-menu {
    right: calc(50% - (var(--column-max) / 2) + var(--space-3));
  }

  /* and the add button floats over the end of the bar, which is the column's end here and not the
     window's: measured from the window it would hang in the empty margin, away from everything */
  .fab {
    right: calc(50% - (var(--column-max) / 2) + var(--space-4));
  }
}

/* ---- wide screens: the standalone desktop app ----
   A phone-width column, however wide the window around it, is not what a desktop app looks like:
   past 960px there is room for a real left side panel, like a console's own — brand, the add
   button, the screens grouped into two, the account at the foot — with the content to its right
   and the topbar carrying the screen's own title instead of the app's.

   html.has-side is the one switch every rule below reads: showNavigation in app.js is the only
   place that sets it, and only outside Telegram, only once the app itself is showing rather than
   the login screen, the "signing you in" of a token, or the welcome guide. Below 960px, and inside
   Telegram whatever the class says, none of this fires — the phone and the Mini App keep the tab
   bar and the 720px centred column above, exactly as they were.

   The side panel itself is the one exception written outside the media query: display:none there
   always, so a phone stuck with the class on it from a resized window still never shows it. */

.side { display: none; }
.side[hidden] { display: none; }

@media (min-width: 960px) {
  html.has-side .side {
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 26;
    width: var(--side-width);
    gap: 18px;
    padding: 20px 14px 16px;
    background: var(--surface);
    border-right: 1px solid var(--border);
    overflow-y: auto;
  }
  html.has-side .side[hidden] { display: none; }

  html.has-side .side__brand {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 6px;
    font-family: var(--font-display);
    font-weight: var(--fw-semibold);
    font-size: 16px;
    color: var(--text);
  }
  html.has-side .side__brand img { flex: none; border-radius: var(--radius-sm); }

  html.has-side .side__add {
    display: flex;
    align-items: center;
    gap: 10px;
    height: 40px;
    padding: 0 12px;
    border: 0;
    border-radius: var(--radius-md);
    background: var(--accent);
    color: var(--text-on-accent);
    font-family: var(--font-sans);
    font-size: 15px;
    font-weight: var(--fw-semibold);
    cursor: pointer;
    transition: background var(--dur) var(--ease-out);
  }
  html.has-side .side__add:hover { background: var(--accent-hover); }
  html.has-side .side__add:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
  html.has-side .side__add-icon { width: 18px; height: 18px; flex: none; }
  /* decorative only — the "N" shortcut itself is bound in app.js, not read off this */
  html.has-side .side__add-kbd {
    margin-left: auto;
    padding: 0 5px;
    border: 1px solid color-mix(in oklab, currentColor 35%, transparent);
    border-radius: 4px;
    font-family: var(--font-mono);
    font-size: 11px;
  }

  /* the same shape as .side__add, right under it — but a ghost rather than a second filled
     button: one solid accent call to action on the panel is enough, and a second one directly
     under the first reads as though the panel could not decide which mattered more */
  html.has-side .side__search {
    display: flex;
    align-items: center;
    gap: 10px;
    height: 40px;
    margin-top: 6px;
    padding: 0 12px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface);
    color: var(--text);
    font-family: var(--font-sans);
    font-size: 15px;
    font-weight: var(--fw-medium);
    cursor: pointer;
    transition: background var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
  }
  html.has-side .side__search:hover { background: var(--surface-2); }
  html.has-side .side__search:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
  html.has-side .side__search .side__add-icon { width: 18px; height: 18px; flex: none; color: var(--text-muted); }

  html.has-side .side__nav { display: flex; flex-direction: column; gap: 18px; }
  html.has-side .side__group { display: flex; flex-direction: column; gap: 2px; }
  html.has-side .side__group-label {
    padding: 0 10px;
    margin-bottom: 2px;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: var(--ls-label);
    text-transform: uppercase;
    color: var(--text-subtle);
  }

  html.has-side .side__item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 8px 10px;
    border: 0;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--text-muted);
    font: inherit;
    font-size: 15px;
    font-weight: 500;
    text-align: left;
    cursor: pointer;
    transition: background var(--dur) var(--ease-out), color var(--dur) var(--ease-out);
  }
  html.has-side .side__item svg { width: 20px; height: 20px; flex: none; opacity: .85; }
  html.has-side .side__item:hover { background: var(--surface-2); color: var(--text); }
  html.has-side .side__item:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
  html.has-side .side__item[aria-current="page"] { background: var(--accent-soft); color: var(--accent); }
  html.has-side .side__item[aria-current="page"] svg { opacity: 1; }

  html.has-side .side__foot { margin-top: auto; display: flex; flex-direction: column; gap: 8px; }

  html.has-side .side__who {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 6px 10px;
    border: 0;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition: background var(--dur) var(--ease-out);
  }
  html.has-side .side__who:hover { background: var(--surface-2); }
  html.has-side .side__who:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
  html.has-side .side__who-avatar {
    flex: none;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--accent-soft);
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 13px;
    font-weight: var(--fw-semibold);
  }
  html.has-side .side__who-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 14px;
  }

  html.has-side body {
    max-width: none;
    margin: 0;
    padding-left: var(--side-width);
  }

  /* the tab bar and the topbar's own menu are the side panel now, and the plain title gives way
     to the topbar's own screen name — see .topbar__screen below. The magnifier goes too: the
     panel carries its own "Cerca", right under #side-add, and the topbar button next to the
     overflow one would only be the same door twice */
  html.has-side .tabs,
  html.has-side #search-open,
  html.has-side #overflow,
  html.has-side .overflow-menu,
  html.has-side .topbar__title {
    display: none;
  }

  /* the title lines up with the content column under it by being that column: the same width and
     the same side padding as .screen below, centred in the same space. A padding worked out from
     100% could not do it — on a fixed element 100% is the window, not the space next to the panel */
  html.has-side .topbar__screen {
    display: block;
    flex: 1 1 auto;
    width: 100%;
    max-width: var(--desk-column);
    margin: 0 auto;
    padding: 0 var(--space-7);
    font-family: var(--font-display);
    font-size: 22px;
    font-weight: var(--fw-semibold);
    color: var(--text);
  }

  html.has-side .topbar {
    left: var(--side-width);
    max-width: none;
    margin: 0;
    height: 64px;
    padding: 0;
    background: color-mix(in oklab, var(--bg) 85%, transparent);
    backdrop-filter: blur(8px);
  }

  html.has-side .screen {
    max-width: var(--desk-column);
    margin: 0 auto;
    padding-top: calc(64px + var(--space-6));
    padding-left: var(--space-7);
    padding-right: var(--space-7);
    /* no tab bar to leave room for any more */
    padding-bottom: var(--space-9);
  }

  html.has-side .notice {
    max-width: var(--desk-column);
    margin-left: auto;
    margin-right: auto;
  }

  /* the desktop bar is 64px tall rather than --topbar-height, so the strip's own sticky offset
     has to follow it here or the strip would sit a phone topbar's worth too high, under the real
     one */
  html.has-side .day-strip { top: 64px; }

  /* the add card, centred as a real dialog rather than sliding up from an edge that is not the
     bottom of anything on a desktop window */
  html.has-side .sheet {
    left: 50%;
    right: auto;
    top: 50%;
    bottom: auto;
    width: 440px;
    max-width: calc(100vw - 48px);
    max-height: min(760px, 90vh);
    margin: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    transform: translate(-50%, -46%) scale(.98);
    opacity: 0;
    transition: transform var(--dur-slow) var(--ease-out), opacity var(--dur-slow) var(--ease-out);
  }
  html.has-side .sheet.is-open {
    transform: translate(-50%, -50%);
    opacity: 1;
  }
  /* nothing is dragged shut here — the grip is what a thumb pulls, and there is no thumb */
  html.has-side .sheet__grip { display: none; }

  /* the category picker, the shared account and the investment position: a right-side drawer over
     the content instead of the phone's own full-screen overlay. They keep their own background
     and padding — only the shape and the place on the window change. */
  html.has-side .picker {
    left: auto;
    right: 0;
    top: 0;
    bottom: 0;
    width: 480px;
    max-width: none;
    margin: 0;
    border-left: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
  }
}

/* ---- wide screens: the expenses in two columns ----
   The expenses screen is the one somebody opens most, and on a desk window it is a long list with
   three small things on top of it — the month, the totals, the subscriptions — pushing it down.
   From 1200px there is room for both side by side: the list on the left, the month and its totals
   on the right, where they stay in view next to the rows they add up.

   The column is wider for this screen only, through the same --desk-column the topbar's title
   reads, so the title stays over the content's left edge. data-open-screen on <html> is set by
   markNavigation in app.js. Between 960 and 1200px the list would be squeezed to a phone's width
   next to the totals, so there the screen stays one column like the others.

   Placed rather than reordered: the markup is the phone's order, and the grid only says which
   column each piece sits in. The list spans every row after the first, the last of them 1fr, so
   the rows on the right are as tall as their own content and not stretched by the list's. */

@media (min-width: 1200px) {
  html.has-side[data-open-screen="expenses"] { --desk-column: 1120px; }

  html.has-side #screen-expenses.is-active {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    grid-template-rows: auto auto auto auto 1fr;
    column-gap: var(--space-7);
    align-items: start;
  }

  html.has-side #screen-expenses > .month { grid-column: 2; grid-row: 1; }
  html.has-side #screen-expenses > .total { grid-column: 2; grid-row: 2; }
  html.has-side #screen-expenses > #subscriptions-fold { grid-column: 2; grid-row: 3; }
  html.has-side #screen-expenses > #start-here { grid-column: 2; grid-row: 4; }

  /* level with the month's arrows, so the list starts where the totals card does */
  html.has-side #screen-expenses > .section-head {
    grid-column: 1;
    grid-row: 1;
    min-height: 40px;
    margin: 0 0 var(--space-5);
  }
  /* the strip takes the row the list used to start on, over the totals card's own row on the
     right — sticky, so it stays put as the list below it scrolls, the same as on a phone */
  html.has-side #screen-expenses > #expenses-day-strip { grid-column: 1; grid-row: 2; }
  html.has-side #screen-expenses > #expenses-list { grid-column: 1; grid-row: 3 / -1; }
}

/* ---- wide screens: the Riepilogo's own grid ----
   The tiles already wrap at two to a row on a phone; from 960px there is room for the five of
   them in the one row they read best in. From 1200px the two month donuts sit side by side too —
   the one column-worth of everything under them, since a period's totals, its bar chart and its
   own category donut are each already wide things in their own right. */

@media (min-width: 960px) {
  html.has-side #screen-dashboard .tiles { grid-template-columns: repeat(5, 1fr); }
  /* five in a row now, so the fifth is back to being one column like the rest of them */
  html.has-side #screen-dashboard .tile:nth-child(5) { grid-column: auto; }
}

@media (min-width: 1200px) {
  html.has-side[data-open-screen="dashboard"] { --desk-column: 1040px; }

  html.has-side #dashboard-pro-content {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--space-7);
    align-items: start;
  }
  /* the two donuts take the two columns of the first row; everything after them — the period's
     title, its totals, its bar chart and its own donut — spans the full width again */
  html.has-side #dashboard-pro-content > :nth-child(n+3) { grid-column: 1 / -1; }
}

/* ---- investments ----
   The total with the chart under it, the positions as rows (a table on a computer), the closed
   ones, and a position's page. Gain is green and loss is red wherever they appear: the same two
   tokens the chart's line uses, so a number and the line it came from are the same colour. */

.inv-gain.is-gain { color: var(--success); }
.inv-gain.is-loss { color: var(--danger); }
.inv-gain { font-weight: var(--fw-semibold); }

/* "Movimento" by the title, for a computer; on a phone the round button over the tab bar says it */
.inv-head { display: none; }

.inv-card { display: flex; flex-direction: column; gap: var(--space-4); }
.inv-card__total { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.inv-card__top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.inv-card__chart { min-width: 0; }
.inv-warn { font-size: var(--fs-caption); color: var(--highlight); }

.inv-pill {
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  white-space: nowrap;
}
.inv-pill.is-up { background: var(--success-soft); color: var(--success); }
.inv-pill.is-down { background: var(--danger-soft); color: var(--danger); }

/* the chart */
.inv-chart { display: flex; flex-direction: column; gap: var(--space-3); }
.inv-chart__periods {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-1);
  padding: 2px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
}
.inv-chart__period {
  min-height: 40px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.inv-chart__period[aria-pressed="true"] {
  border-color: var(--border);
  background: var(--surface);
  color: var(--text);
  font-weight: var(--fw-bold);
  box-shadow: var(--shadow-xs);
}
.inv-chart__period:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.inv-chart__caption {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  min-height: 22px;
  font-size: var(--fs-body-sm);
}
.inv-chart__caption[hidden] { display: none; }
.inv-chart__when { color: var(--text-muted); }
.inv-chart__figure { font-family: var(--font-mono); font-weight: var(--fw-semibold); white-space: nowrap; }
.inv-chart__figure .is-gain { color: var(--success); }
.inv-chart__figure .is-loss { color: var(--danger); }
.inv-chart__plot {
  position: relative;
  height: 180px;
  touch-action: none;
  cursor: crosshair;
  -webkit-user-select: none;
  user-select: none;
}
.inv-chart__plot[hidden] { display: none; }
.inv-chart__svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.inv-chart__svg path, .inv-chart__svg line { vector-effect: non-scaling-stroke; }
.inv-chart__grid { fill: none; stroke: var(--border-faint); stroke-width: 1; }
.inv-chart__areaUp { fill: var(--success-soft); stroke: none; }
.inv-chart__areaDown { fill: var(--danger-soft); stroke: none; }
.inv-chart__cost { fill: none; stroke: var(--text-muted); stroke-width: 1.5; stroke-dasharray: 4 4; }
.inv-chart__level { fill: none; stroke: var(--highlight); stroke-width: 2; stroke-dasharray: 0.5 5; stroke-linecap: round; }
.inv-chart__lineUp, .inv-chart__lineDown { fill: none; stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; }
.inv-chart__lineUp { stroke: var(--success); }
.inv-chart__lineDown { stroke: var(--danger); }
.inv-chart__cursor { stroke: var(--text-muted); stroke-width: 1; opacity: 0; }
.inv-chart__labels { position: absolute; inset: 0; pointer-events: none; }
.inv-chart__label {
  position: absolute;
  font-family: var(--font-mono);
  font-size: var(--fs-mono-label);
  font-weight: var(--fw-medium);
  line-height: 14px;
  color: var(--text-muted);
  white-space: nowrap;
}
.inv-chart__label--grid { transform: translateY(-110%); }
.inv-chart__label--tick { top: calc(100% - 18px); }
.inv-chart__label--level { transform: translateY(-115%); color: var(--highlight); }
.inv-chart__dot {
  position: absolute;
  width: 11px;
  height: 11px;
  margin: -5.5px 0 0 -5.5px;
  border-radius: 50%;
  border: 2px solid var(--surface);
  pointer-events: none;
}
.inv-chart__dot.is-up { background: var(--success); }
.inv-chart__dot.is-down { background: var(--danger); }
.inv-chart__empty { padding: var(--space-3) 0; color: var(--text-muted); font-size: var(--fs-body-sm); }
.inv-chart__empty[hidden] { display: none; }
.inv-chart__legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
  color: var(--text-muted);
  font-size: var(--fs-caption);
}
.inv-chart__legend[hidden] { display: none; }
.inv-chart__key { display: inline-flex; align-items: center; gap: 6px; }
.inv-chart__key::before { content: ""; width: 22px; height: 0; border-top: 2px solid var(--success); }
.inv-chart__key + .inv-chart__key::before { border-top: 2px dashed var(--text-muted); }

/* a row of the positions, open or closed */
.card.inv-row { display: flex; align-items: center; gap: var(--space-3); }
.inv-row__initial {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  color: var(--text-muted);
  font-weight: var(--fw-semibold);
}
.inv-row__main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.inv-row__name { font-weight: var(--fw-semibold); }
.inv-row__meta { color: var(--text-muted); font-size: var(--fs-caption); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.inv-row__warn { color: var(--highlight); font-size: var(--fs-caption); }
.inv-row__side { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-1); }
.inv-row__value { font-family: var(--font-mono); font-weight: var(--fw-bold); font-size: var(--fs-body-sm); }

.inv-closed-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: 44px;
  margin: var(--gap-section) 0 var(--space-1);
  padding: 0 var(--space-1);
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.inv-closed-toggle[hidden] { display: none; }
.inv-closed-toggle:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: var(--radius-md); }
.inv-closed-toggle .section-head__count::after { content: " \25BE"; }
.inv-closed-toggle[aria-expanded="true"] .section-head__count::after { content: " \25B4"; }
#investments-closed-list[hidden] { display: none; }

/* the table is for a computer; a phone has the rows */
.inv-table { display: none; }

/* a position's page */
.inv-title { min-width: 0; display: flex; flex-direction: column; }
.inv-title__name { font-family: var(--font-display); font-size: var(--fs-h4); font-weight: var(--fw-bold); }
.inv-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--space-4); padding-top: var(--space-1); padding-bottom: var(--space-1); }
.inv-fact { display: flex; flex-direction: column; padding: var(--space-2) 0; border-bottom: 1px solid var(--border-faint); }
.inv-fact__label { color: var(--text-muted); font-size: var(--fs-caption); }
.inv-fact__value { font-family: var(--font-mono); font-weight: var(--fw-bold); font-size: var(--fs-body-sm); }
.inv-note { padding: var(--space-2) var(--space-1) 0; }

/* the round add button, with a word on this tab */
.fab__label { display: none; }
html[data-open-screen="investments"] .fab {
  width: auto;
  min-width: var(--fab-size);
  padding: 0 var(--space-5) 0 var(--space-4);
  gap: var(--space-2);
}
html[data-open-screen="investments"] .fab__label {
  display: inline;
  font-size: var(--fs-body);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-snug);
}

@media (min-width: 960px) {
  html.has-side[data-open-screen="investments"] { --desk-column: 1040px; }

  html.has-side .inv-head { display: flex; justify-content: flex-end; margin-bottom: var(--space-4); }
  html.has-side .inv-head .button { width: auto; }

  /* the chart beside the total, as the design draws it */
  html.has-side #screen-investments .inv-card { flex-direction: row; align-items: stretch; gap: var(--space-6); }
  html.has-side #screen-investments .inv-card__total { flex: 1 1 220px; }
  html.has-side #screen-investments .inv-card__chart { flex: 3 1 420px; }
  html.has-side .inv-chart__plot { height: 240px; }
  html.has-side #investments-list.has-table { display: none; }

  html.has-side .inv-table {
    display: block;
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
  }
  html.has-side .inv-table:empty { display: none; }
  .inv-table__grid { width: 100%; min-width: 680px; border-collapse: collapse; font-size: var(--fs-body-sm); }
  .inv-table__grid th, .inv-table__grid td {
    padding: var(--space-3) var(--space-4);
    text-align: right;
    font-family: var(--font-mono);
    white-space: nowrap;
  }
  .inv-table__grid th { font-family: var(--font-sans); font-weight: var(--fw-medium); font-size: var(--fs-caption); color: var(--text-muted); }
  .inv-table__grid thead th { border-bottom: 1px solid var(--border); }
  .inv-table__grid tbody td { border-bottom: 1px solid var(--border-faint); }
  .inv-table__grid tfoot th, .inv-table__grid tfoot td { font-weight: var(--fw-semibold); }
  .inv-table__grid .is-left { text-align: left; font-family: var(--font-sans); }
  .inv-table__row { cursor: pointer; }
  .inv-table__row:hover { background: var(--surface-2); }
  .inv-table__name {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
  }
  .inv-table__name:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: var(--radius-sm); }
  .inv-table__muted { color: var(--text-muted); }
  .inv-table__strong { font-weight: var(--fw-bold); }
}

/* ---- less motion ----
   Somebody who asked the system for less of it gets none: everything above still arrives, it just
   arrives already there. This is one block at the end rather than a rule at each place, so a thing
   that starts moving cannot be added without this catching it. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}
