/*
 * This is a manifest file that'll be compiled into application.css.
 *
 * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
 * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
 * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
 * depending on specificity.
 *
 * Consider organizing styles into separate files for maintainability.
 */

/* Every colour is one light-dark() pair, so a token is defined once and the
   theme picks a side. What picks the side is the color-scheme below: the page
   follows the operating system until the reader says otherwise, and the two
   data-theme rules are that override. Nothing else in this file needs to know
   which theme is on. */
/* ---------- Typeface ---------- */

/* Lora, served by the app itself (app/assets/fonts/lora, under the SIL Open
   Font License that sits beside it): no request to a third party, nothing for
   a content security policy to allow, and it works offline. One variable file
   per script covers every weight the app uses, 400 to 700. The browser fetches
   latin-ext only for a page with a character from it (a name with "ł", say),
   and "swap" shows text at once in the fallback, trading it for Lora the
   moment the file is in: the layout preloads the latin one, so on most visits
   that is before anything is drawn. Italics are left out: nothing is set in
   them.

   Lora's tabular figures are wide, so they are kept for numbers that stand in
   a column and must line up (the ledger, the chart's legend, a card's
   instalments). A number standing on its own, like a month's total, uses
   Lora's ordinary figures, which do not leave gaps around a 1. */
@font-face {
  font-family: "Lora";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/assets/lora/lora-latin-ext-29c268c9.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Lora";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/assets/lora/lora-latin-fce3b577.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  color-scheme: light dark;

  --ink: light-dark(#1a1a1a, #ececec);
  --ink-muted: light-dark(#6b6b6b, #9d9d9d);
  --line: light-dark(#dcdcdc, #303030);
  --line-strong: light-dark(#b8b8b8, #4a4a4a);
  --surface: light-dark(#ffffff, #1a1a1a);
  --canvas: light-dark(#fafafa, #121212);
  /* The accent carries the page's text colour, so it flips with it, and
     anything painted with it needs the opposite ink on top. */
  --accent: light-dark(#1a1a1a, #ececec);
  --accent-hover: light-dark(#000000, #ffffff);
  --accent-ink: light-dark(#ffffff, #121212);
  --danger: light-dark(#b3261e, #f2857c);
  --danger-hover: light-dark(#8f1e17, #ffa79f);
  --danger-surface: light-dark(#fdecea, #3a1512);
  --success: light-dark(#1e6b3a, #6ec894);
  --success-surface: light-dark(#eaf5ee, #102b1b);
  --warning: light-dark(#8a5300, #e0a94a);
  --warning-surface: light-dark(#fff4e5, #33240c);
  --brand: light-dark(#3b5bfd, #8a9bff);
  --brand-surface: light-dark(#eef1ff, #1b2042);
  /* Placeholder fill for an avatar with nothing in it yet. */
  --placeholder: light-dark(#c9c9c9, #3a3a3a);
  /* Dims everything behind a dialog: darker in the dark theme, because the
     page it covers is already dark. */
  --overlay: light-dark(rgb(0 0 0 / 40%), rgb(0 0 0 / 65%));
  /* One page width, shared by the nav bar's contents and the page below it, so
     the two can never drift apart. */
  --page-max: 2180px;
  --page-gutter: 20px;
  --radius: 12px;
  --radius-card: 16px;
  --radius-pill: 999px;
  /* Barely there on purpose: it lifts a card off the canvas without the page
     turning into a stack of floating panels. A shadow on a dark canvas reads
     as nothing at all, so it goes heavier there to stay visible. */
  --shadow-card: 0 1px 2px light-dark(rgb(16 24 40 / 5%), rgb(0 0 0 / 40%));
  --shadow-pop: 0 10px 30px light-dark(rgb(0 0 0 / 12%), rgb(0 0 0 / 55%));
  --shadow-float: 0 6px 20px light-dark(rgb(0 0 0 / 12%), rgb(0 0 0 / 50%));
  --shadow-field: 0 2px 8px light-dark(rgb(16 24 40 / 8%), rgb(0 0 0 / 45%));

  /* The scales the screens are built from. A rule that is written or reworked
     takes its lengths from here, so two panels cannot end up 18px and 20px
     apart by accident. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;

  --text-xs: 12px;
  --text-sm: 13px;
  --text-md: 14px;
  --text-base: 15px;
  --text-lg: 18px;
  --text-xl: 24px;
  --text-2xl: 30px;

  --radius-sm: 8px;

  /* Anything a finger has to hit is at least this big where a finger is what
     hits it: 44px is the size the platforms themselves use. */
  --target: 40px;
  --target-touch: 44px;
  /* A row's own buttons (Edit, Delete, the ledger's ⋯) are smaller under a
     mouse, where a list of them should not crowd the row. */
  --row-action-size: 36px;

  /* A strong ease-out: movement starts at once and settles, so the interface
     reads as answering rather than thinking. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  /* The curve iOS sheets move on: quick off the edge, long soft landing. */
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
  /* For something that moves from one place on screen to another rather than
     arriving: it gathers speed and settles, both firmly. */
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --duration-press: 120ms;
  --duration-fast: 160ms;
}

@media (pointer: coarse) {
  :root {
    --row-action-size: var(--target-touch);
  }
}

/* The reader's own choice, kept in localStorage and written onto <html> before
   the first paint. Naming a single scheme here is what makes light-dark() stop
   listening to the operating system. */
:root[data-theme="light"] {
  color-scheme: light;
}

:root[data-theme="dark"] {
  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

/* A tap paints no grey box of its own: every control below has its own
   pressed state. Text keeps its size when a phone turns sideways. */
html {
  -webkit-tap-highlight-color: transparent;
  -webkit-text-size-adjust: 100%;
  /* Scrollbars in the page's own colours, a quiet thumb on no track, so a
     scrolling modal body does not show a white bar in the dark theme. Every
     scrolling box inherits it. */
  scrollbar-color: var(--line-strong) transparent;
}

/* Taps fire at once instead of waiting to see whether a double-tap zoom is on
   its way. */
a,
button,
label,
select,
summary,
[role="button"] {
  touch-action: manipulation;
}

/* Controls take the page's typeface rather than the browser's own. Only the
   family: their sizes stay whatever each component sets. */
button,
input,
select,
textarea {
  font-family: inherit;
}

body {
  margin: 0;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--canvas);
  color: var(--ink);
  /* Until Lora arrives, and wherever it cannot, the nearest serif the system
     has: Apple's New York, then Georgia. */
  font-family: "Lora", ui-serif, Georgia, "Times New Roman", serif;
  font-size: 15px;
  line-height: 1.5;
}

a {
  color: var(--ink);
}

/* One size for every page title, the dashboard's greeting included, and below
   the money on the dashboard: the numbers are what the page is about. */
h1 {
  font-size: var(--text-xl);
  line-height: 1.2;
  letter-spacing: -0.02em;
}

.icon {
  width: 1.05em;
  height: 1.05em;
  flex: none;
}

.main {
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* With viewport-fit=cover the page may run under a notch or a rounded corner,
   so each side's gutter is at least that side's inset. */
:root {
  --gutter-left: max(var(--page-gutter), env(safe-area-inset-left, 0px));
  --gutter-right: max(var(--page-gutter), env(safe-area-inset-right, 0px));
}

/* Wide enough for the chart to sit beside the ledger without squeezing it:
   the ledger column lands at roughly the 900px this used to be in total. */
.is-authenticated .main {
  display: block;
  width: 100%;
  max-width: var(--page-max);
  margin: 0 auto;
  padding: var(--space-6) var(--gutter-right) calc(48px + env(safe-area-inset-bottom, 0px)) var(--gutter-left);
}

/* It takes focus only from the skip link, and a ring round the whole page
   says nothing. */
.main:focus {
  outline: none;
}

/* ---------- Skip link ---------- */

/* Out of sight until the keyboard reaches it, then first thing on screen. No
   transition: it is only ever summoned by a key. */
.skip-link {
  position: absolute;
  top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
  left: var(--gutter-left);
  z-index: 200;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--text-md);
  text-decoration: none;
  translate: 0 calc(-100% - var(--space-6));
}

.skip-link:focus {
  translate: 0 0;
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- Sideways scrollers ---------- */

/* A line that scrolls sideways fades out at whichever end still hides
   something (the scroll-edges controller says which), and nowhere else: a fade
   over the last item, with nothing past it, only looks broken. */
.scroll-edges[data-scroll="start"] {
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}

.scroll-edges[data-scroll="middle"] {
  mask-image: linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent);
}

.scroll-edges[data-scroll="end"] {
  mask-image: linear-gradient(to right, transparent, #000 28px);
}

/* ---------- Pressing ---------- */

/* Every small control a finger or a click presses sinks a little under it,
   as the buttons do, so the press is answered the moment it lands. They take
   `scale` rather than `transform`, which the field toggles already use to
   centre themselves. The pickers and the segmented choice carry their own
   transitions further down, with this one in them. */
.menu__kebab,
.modal__close,
.period__step,
.field__toggle {
  transition: scale var(--duration-press) var(--ease-out);
}

@media (prefers-reduced-motion: no-preference) {
  .menu__kebab:active,
  .modal__close:active,
  .period__step:active,
  .field__toggle:active,
  .icon-picker__label:active,
  .tag-picker__label:active,
  .colour-picker__label:active {
    scale: 0.95;
  }
}

/* ---------- Frames ---------- */

/* While Turbo fetches a frame's new contents (a filtered list, the ledger, a
   purchase's instalments), the old ones fade back so they are not mistaken for
   the answer. The delay keeps a quick reply from flickering. */
turbo-frame {
  display: block;
  transition: opacity var(--duration-fast) ease;
}

turbo-frame[aria-busy="true"] {
  opacity: 0.45;
  transition-delay: 150ms;
}

/* ---------- Page transitions ---------- */

/* Turbo draws every visit inside a view transition (the meta tag in the
   layout), and every visit but one stays as instant as it was: only a step
   between months moves (the month-step controller marks it). The bar and the
   page's heading stay where they are, and the month under them leaves to one
   side while the new one comes in from the side it lies on. */
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
  animation: none;
}

/* Named only for the step: a name makes an element a stacking context, and
   the account menu's panel must be free to overlap the page the rest of the
   time. */
:root[data-month-step] .nav {
  view-transition-name: site-nav;
}

:root[data-month-step] .page-head {
  view-transition-name: page-head;
}

/* The heading's month changes in place; the old words must not show through. */
::view-transition-old(site-nav),
::view-transition-old(page-head) {
  display: none;
}

/* A theme change reveals the new page over the old one (theme controller):
   the two pictures are laid one on the other, not added together. */
:root[data-theme-changing]::view-transition-old(root),
:root[data-theme-changing]::view-transition-new(root) {
  mix-blend-mode: normal;
}

/* What the sliding month uncovers at its edges is the page's own background. */
:root[data-month-step]::view-transition {
  background: var(--canvas);
}

@media (prefers-reduced-motion: no-preference) {
  :root[data-month-step="next"]::view-transition-old(root) {
    animation: month-leave-left 160ms var(--ease-out) both;
  }

  :root[data-month-step="next"]::view-transition-new(root) {
    animation: month-enter-from-right 240ms var(--ease-out) both;
  }

  :root[data-month-step="previous"]::view-transition-old(root) {
    animation: month-leave-right 160ms var(--ease-out) both;
  }

  :root[data-month-step="previous"]::view-transition-new(root) {
    animation: month-enter-from-left 240ms var(--ease-out) both;
  }
}

/* Reduced motion keeps the crossfade and loses the sliding. */
@media (prefers-reduced-motion: reduce) {
  :root[data-month-step]::view-transition-old(root) {
    animation: month-fade-out 120ms ease both;
  }

  :root[data-month-step]::view-transition-new(root) {
    animation: month-fade-in 160ms ease both;
  }
}

@keyframes month-leave-left {
  to { opacity: 0; translate: -24px 0; }
}

@keyframes month-leave-right {
  to { opacity: 0; translate: 24px 0; }
}

@keyframes month-enter-from-right {
  from { opacity: 0; translate: 24px 0; }
}

@keyframes month-enter-from-left {
  from { opacity: 0; translate: -24px 0; }
}

@keyframes month-fade-out {
  to { opacity: 0; }
}

@keyframes month-fade-in {
  from { opacity: 0; }
}

/* ---------- Nav ---------- */

/* The bar itself runs the full width, because the rule under it belongs to the
   window rather than to the page. */
.nav {
  padding-top: env(safe-area-inset-top, 0px);
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

/* Its contents stop where the page does: the links have to start above the
   first card, not out at the window's edge. */
.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  width: 100%;
  min-height: 60px;
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 0 var(--gutter-right) 0 var(--gutter-left);
}

/* Tabs rather than underlined links. They run the nav's full height, so each
   is a comfortable target, and they scroll sideways when there are more than
   fit (see the phone rules below). */
.nav__links {
  position: relative;
  display: flex;
  align-self: stretch;
  gap: var(--space-1);
  min-width: 0;
  margin-left: calc(var(--space-3) * -1);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}

.nav__link {
  position: relative;
  display: flex;
  align-items: center;
  padding: 0 var(--space-3);
  color: var(--ink-muted);
  font-size: var(--text-md);
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  user-select: none;
}

/* Where the reader is: full ink and a rule along the nav's bottom edge. */
.nav__link[aria-current="page"] {
  color: var(--ink);
}

.nav__link[aria-current="page"]::after {
  content: "";
  position: absolute;
  right: var(--space-3);
  bottom: 0;
  left: var(--space-3);
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .nav__link:hover {
    color: var(--ink);
  }
}

.nav__link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -6px;
  border-radius: var(--radius-sm);
}

.nav__account {
  display: flex;
  flex: none;
  align-items: center;
}

.nav__user {
  color: var(--ink-muted);
  font-size: var(--text-md);
}

/* A phone gets one line, not two: the links scroll on their own, faded at the
   edge where there is more, and the account menu keeps only its avatar. */
@media (max-width: 640px) {
  .nav__inner {
    gap: var(--space-2);
  }

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

  .menu__trigger .menu__chevron {
    display: none;
  }
}

/* ---------- Account menu ---------- */

.menu {
  position: relative;
}

.menu__trigger {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--target);
  padding: 4px 10px 4px 4px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  user-select: none;
}

.menu__trigger[aria-expanded="true"] {
  border-color: var(--line-strong);
  background: var(--surface);
}

@media (hover: hover) and (pointer: fine) {
  .menu__trigger:hover {
    border-color: var(--line-strong);
    background: var(--surface);
  }
}

.menu__trigger:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.menu__avatar {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 13px;
  font-weight: 600;
}

.menu__chevron {
  color: var(--ink-muted);
  transition: transform var(--duration-fast) var(--ease-out);
}

.menu__trigger[aria-expanded="true"] .menu__chevron {
  transform: rotate(180deg);
}

/* On a phone the trigger is only the avatar, still a full-size target. */
@media (max-width: 640px) {
  .menu__trigger {
    justify-content: center;
    min-width: var(--target-touch);
    min-height: var(--target-touch);
    padding: 2px;
  }
}

.menu__panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  min-width: 220px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-pop);
}

/* An explicit display beats the hidden attribute, so put it back. */
.menu__panel[hidden] {
  display: none;
}

/* A menu grows out of the button that opened it (both open below and to the
   left of it, so from the top right corner), and folds back into it a little
   quicker. A row's menu is opened many times a day, so it moves faster still:
   enough to show where it came from, too little to wait for. */
.menu__panel {
  transform-origin: top right;
  transition: opacity 150ms var(--ease-out), scale 150ms var(--ease-out), display 150ms allow-discrete;
}

.menu__panel[hidden] {
  opacity: 0;
  scale: 0.97;
  transition-duration: 100ms;
}

.menu--row .menu__panel {
  transition-duration: 110ms;
}

.menu--row .menu__panel[hidden] {
  transition-duration: 80ms;
}

@starting-style {
  .menu__panel {
    opacity: 0;
    scale: 0.97;
  }
}

/* Reduced motion keeps the fade and loses the growing. */
@media (prefers-reduced-motion: reduce) {
  .menu__panel,
  .menu__panel[hidden] {
    scale: 1;
  }

  @starting-style {
    .menu__panel {
      scale: 1;
    }
  }

  .menu__chevron {
    transition: none;
  }
}

.menu__identity {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: var(--space-2) 10px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 6px;
  font-size: var(--text-sm);
}

.menu__identity span {
  color: var(--ink-muted);
  overflow-wrap: anywhere;
}

.menu__identity .menu__role {
  margin-top: var(--space-1);
  background: var(--brand-surface);
  color: var(--brand);
}

.menu__item {
  display: flex;
  align-items: center;
  min-height: var(--target);
  padding: 0 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: var(--text-md);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
}

@media (hover: hover) and (pointer: fine) {
  .menu__item:hover {
    background: var(--canvas);
  }

  .menu__item--danger:hover {
    background: var(--danger-surface);
  }
}

.menu__item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.menu__form {
  display: contents;
}

.menu__item--danger {
  width: 100%;
  color: var(--danger);
}

/* A block inside the menu that is a setting rather than a destination, so it
   gets a rule above it and a name of its own. */
.menu__section {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px;
  border-top: 1px solid var(--line);
  margin-top: 6px;
}

.menu__label {
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* ---------- Theme switch ---------- */

/* A component of its own. The entry form's segmented choice further down looks
   alike but is built differently, and while both were called .segmented the
   later one quietly restyled this. */
.theme-switch {
  position: relative;
  display: flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--canvas);
}

/* The lifted pill travels to the chosen option, as in the entry form's
   segmented choice: three answers, a third of the width less the gaps. */
.theme-switch::before {
  content: "";
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 2px;
  width: calc((100% - 8px) / 3);
  border-radius: var(--radius-pill);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  transition: translate 220ms var(--ease-in-out);
}

.theme-switch:has(> :nth-child(2)[aria-pressed="true"])::before {
  translate: calc(100% + 2px) 0;
}

.theme-switch:has(> :nth-child(3)[aria-pressed="true"])::before {
  translate: calc(200% + 4px) 0;
}

.theme-switch:not(:has([aria-pressed="true"]))::before {
  opacity: 0;
}

/* While the new theme is being revealed the page is a picture of itself, so
   the pill has to be where it ends up when that picture is taken. */
:root[data-theme-changing] .theme-switch::before {
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .theme-switch::before {
    transition: none;
  }
}

.theme-switch__option {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  flex: 1;
  min-height: 32px;
  padding: 5px 8px;
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--ink-muted);
  font: inherit;
  font-size: var(--text-xs);
  cursor: pointer;
  user-select: none;
  transition: color var(--duration-fast) ease, scale var(--duration-press) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .theme-switch__option:hover {
    color: var(--ink);
  }
}

/* The chosen one is the only one lifted (the pill above): that is the whole
   state, and it is read off aria-pressed rather than a class, so the button
   says the same thing to a screen reader as it does to an eye. */
.theme-switch__option[aria-pressed="true"] {
  color: var(--ink);
}

@media (prefers-reduced-motion: no-preference) {
  .theme-switch__option:active {
    scale: 0.97;
  }
}

.theme-switch__option:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.theme-switch__option .icon {
  width: 14px;
  height: 14px;
}

/* ---------- Page head and filters ---------- */

.page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  min-height: var(--target-touch);
}

.page-head h1 {
  margin: 0;
}

.filters {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: var(--space-5);
}

.filters__search,
.filters__select {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* The search box takes whatever the selects leave behind. */
.filters__search {
  flex: 1 1 220px;
}

.filters__select {
  flex: 0 0 auto;
}

.filters__select .field__input {
  min-width: 140px;
}

/* On a phone the search has a line of its own and the selects share the one
   under it, as long as their words fit; when they do not, each gets a line
   rather than a label cut short. */
@media (max-width: 560px) {
  .filters__search {
    flex-basis: 100%;
  }

  .filters__select {
    flex: 1 1 auto;
    min-width: 0;
  }

  .filters__select .field__input {
    min-width: 0;
  }
}

/* The ledger's bar: search across the top with its way out beside it, and the
   selects on one line underneath. When the panel is too narrow for them, that
   line scrolls on its own, and the page never does. */
.filters--ledger {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.filters--ledger .filters__search {
  grid-column: 1;
}

.filters--ledger .filters__clear {
  grid-row: 1;
  grid-column: 2;
  align-self: center;
}

.filters__selects {
  grid-column: 1 / -1;
  display: flex;
  gap: var(--space-2);
  /* Room for the focus ring, which an overflow container would otherwise clip. */
  margin: -3px;
  padding: 3px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}

.filters__selects .filters__select {
  flex: 1 0 auto;
}

.filters__selects .field__input {
  min-width: 0;
  padding-block: 9px;
}

/* A filter that is on looks on, so a narrowed list never passes for the whole
   of it. */
.filters select.field__input:has(option:checked:not([value=""])) {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.search__icon {
  position: absolute;
  top: 50%;
  left: 12px;
  transform: translateY(-50%);
  color: var(--ink-muted);
  pointer-events: none;
}

/* WebKit adds its own clear button to type="search"; this file supplies one. */
.search input[type="search"]::-webkit-search-cancel-button {
  appearance: none;
}

.search input[type="search"]:placeholder-shown + .search__clear {
  display: none;
}

/* ---------- Tables and detail lists ---------- */

table {
  width: 100%;
  margin-top: 16px;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

th,
td {
  padding: 10px 14px;
  text-align: left;
  border-bottom: 1px solid var(--line);
}

th {
  color: var(--ink-muted);
  font-size: 13px;
  font-weight: 500;
}

tbody tr:last-child td {
  border-bottom: 0;
}

td form.button_to {
  display: inline;
}

.category__name {
  display: flex;
  align-items: center;
  gap: 10px;
}

.category__icon {
  width: 20px;
  height: 20px;
  color: var(--ink-muted);
}

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

/* Categories, tags and users: the ledger's card and head, and a table that
   takes its shape from the card's width rather than the window's. */
.list-panel {
  container: records / inline-size;
  margin-top: var(--space-4);
}

.records {
  margin-top: 0;
}

.records th {
  white-space: nowrap;
}

/* The name may wrap; a badge, a count or an address broken in two reads as
   two things, so the rest stay on one line. */
.records td:not(.records__main) {
  white-space: nowrap;
}

.records__muted {
  color: var(--ink-muted);
}

/* A name is what the row is, not a link to be told apart from the text: it
   shows it is one under the pointer, like the ledger's descriptions. */
.records__main a {
  font-weight: 500;
  text-decoration: none;
}

.records__main a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .records__main a:not(.records__chip):hover {
    text-decoration: underline;
  }

  .records tbody tr:hover > td:not(.table__empty) {
    background: var(--canvas);
  }
}

/* As wide as its buttons and no wider: the room goes to the other columns.
   As tall as them even when there are none (an admin's own row), so every row
   is the same height. */
.records__actions {
  width: 1%;
}

.records tbody .records__actions {
  height: calc(var(--row-action-size) + 20px);
}

/* The words go first when the table gets tight; the buttons keep their icons
   and are still read out in full. */
@container records (max-width: 800px) {
  .records .row-action__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* Too narrow for columns, each record becomes a list row, as on a phone: the
   name on top, what the other columns said on the line under it, and the
   buttons at the end, centred on both lines. Nothing is dropped and nothing
   scrolls sideways. */
@container records (max-width: 600px) {
  .records {
    --row-actions-width: calc(var(--row-action-size) * 2 + var(--space-1));

    display: block;
    /* Edge to edge inside the card, like the ledger on a phone. */
    width: calc(100% + var(--panel-pad) * 2);
    margin: calc(var(--space-2) * -1) calc(var(--panel-pad) * -1);
  }

  .records tbody {
    display: block;
  }

  /* Column names mean nothing without columns; they are still read out. */
  .records thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .records tr {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px var(--space-2);
    min-height: calc(var(--row-action-size) + var(--space-3) * 2);
    padding: var(--space-3) calc(var(--panel-pad) + var(--row-actions-width) + var(--space-3))
      var(--space-3) var(--panel-pad);
    border-bottom: 1px solid var(--line);
  }

  .records tbody tr:last-child {
    border-bottom: 0;
  }

  .records td {
    display: block;
    padding: 0;
    border: 0;
  }

  .records__main {
    flex: 1 1 100%;
    min-width: 0;
  }

  .records td:not(.records__main, .records__actions) {
    font-size: var(--text-sm);
  }

  .records__email {
    flex-basis: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .records tbody .records__actions {
    position: absolute;
    inset-block: 0;
    right: var(--panel-pad);
    display: flex;
    align-items: center;
    width: auto;
    height: auto;
  }

  .records tr:has(.table__empty) {
    padding-right: var(--panel-pad);
  }

  .records .table__empty {
    flex: 1 1 100%;
    padding-block: var(--space-6);
  }

  /* A subcategory is indented as a whole, its second line with it. */
  .records .category--child {
    padding-left: calc(var(--panel-pad) + 20px);
  }

  .records .category--child td:first-child {
    padding-left: 0;
  }

  /* Where names have an icon before them, the second line starts under the
     name rather than under the icon (20px icon, 10px gap). */
  .records--icons td:nth-child(2) {
    margin-left: 30px;
  }
}

/* ---------- Main screen ---------- */

.greeting h1 {
  margin: 0;
}

.greeting__note {
  margin: 2px 0 0;
  color: var(--ink-muted);
  font-size: 14px;
}

.period__nav {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.period__step {
  display: grid;
  place-items: center;
  width: var(--target);
  height: var(--target);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink-muted);
  text-decoration: none;
}

.period__step:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .period__step:hover {
    border-color: var(--line-strong);
    color: var(--ink);
  }
}

@media (pointer: coarse) {
  .period__step {
    width: var(--target-touch);
    height: var(--target-touch);
  }
}

/* Only the first letter goes up: "Setembro de 2026", not "Setembro De 2026".
   The month keeps its lowercase everywhere else it is written. */
.period__label {
  min-width: 9ch;
  padding: 0 var(--space-2);
  font-size: var(--text-md);
  text-align: center;
  white-space: nowrap;
}

.period__label::first-letter {
  text-transform: uppercase;
}

.period__today {
  margin-right: var(--space-2);
}

.summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-4);
  margin-top: var(--space-5);
}

.summary__card {
  padding: var(--space-4) var(--space-5);
}

/* The label and its trend badge sit on one line, pushed apart, so the three
   cards line their numbers up whatever the label's length. */
.summary__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 22px;
}

.summary__label {
  margin: 0;
  color: var(--ink-muted);
  font-size: 13px;
}

.summary__value {
  margin: 6px 0 0;
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -0.02em;
}

/* The balance is the number the screen is about, so it gets the one colour
   nothing else on the page uses. It gives that up when it goes negative:
   overdrawn has to read as overdrawn, and red outranks brand. */
.summary__value--headline {
  color: var(--brand);
}

.summary__value--headline.is-negative {
  color: var(--danger);
}

.trend {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 3px 8px;
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
}

.trend.is-good {
  background: var(--success-surface);
  color: var(--success);
}

.trend.is-bad {
  background: var(--danger-surface);
  color: var(--danger);
}

.trend__figure {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}

.trend__arrow {
  width: 9px;
  height: 9px;
  flex: none;
}

.summary__note {
  margin: 4px 0 0;
  color: var(--ink-muted);
  font-size: 12px;
}

.summary__split {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
}

.summary__split strong {
  color: var(--ink);
}

.is-income {
  color: var(--success);
}

.is-expense,
.is-negative {
  color: var(--danger);
}

/* ---------- Quick actions ---------- */

/* Doors into the modals, so they are drawn lighter than the numbers above
   them: a slim row of actions, not three more cards of data. */
.quick {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.quick__card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  text-decoration: none;
  user-select: none;
  transition: border-color var(--duration-fast) ease, box-shadow var(--duration-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .quick__card:hover {
    border-color: var(--line-strong);
    box-shadow: var(--shadow-field);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .quick__card {
    transition: border-color var(--duration-fast) ease, box-shadow var(--duration-fast) ease,
      transform var(--duration-press) var(--ease-out);
  }

  .quick__card:active {
    transform: scale(0.98);
  }
}

.quick__card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.quick__icon {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: 50%;
}

.quick__icon .icon {
  width: 20px;
  height: 20px;
}

.quick__label {
  font-size: var(--text-md);
}

.quick__icon.is-income {
  background: var(--success-surface);
  color: var(--success);
}

.quick__icon.is-expense {
  background: var(--danger-surface);
  color: var(--danger);
}

.quick__icon.is-transfer {
  background: var(--brand-surface);
  color: var(--brand);
}

.quick__text {
  display: flex;
  flex-direction: column;
  line-height: 1.35;
}

.quick__note {
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* A phone has room for the three doors side by side if each one stands up:
   icon over its name, and the note left to the modal that opens. Balance keeps
   the whole width; income and expenses share a row under it. */
@media (max-width: 560px) {
  .summary {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3);
  }

  .summary__card:first-child {
    grid-column: 1 / -1;
  }

  .summary__card {
    padding: var(--space-3) var(--space-4);
  }

  .summary__card:not(:first-child) .summary__value {
    font-size: 20px;
  }

  .summary__card:not(:first-child) .summary__head {
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-2);
  }

  .quick {
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    grid-template-columns: none;
    gap: var(--space-2);
  }

  .quick__card {
    flex-direction: column;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-2);
    text-align: center;
  }

  .quick__label {
    font-size: var(--text-sm);
    line-height: 1.25;
  }

  .quick__note {
    display: none;
  }
}

/* ---------- Chart and ledger ---------- */

/* The chart keeps a fixed column and the ledger takes the rest, so widening
   the window grows the table rather than the doughnut. */
.dash {
  display: grid;
  grid-template-columns: 320px minmax(0, 1fr);
  align-items: start;
  gap: 16px;
  margin-top: 16px;
}

.chart {
  padding: 20px;
}

.chart__ring {
  display: grid;
  place-items: center;
  padding: 12px 0 4px;
}

/* The ring and whatever its middle is saying, sized as one. */
.chart__dial {
  position: relative;
  width: 100%;
  max-width: 200px;
}

.donut {
  display: block;
  width: 100%;
  height: auto;
}

.donut__track {
  fill: none;
  stroke: var(--canvas);
  stroke-width: 5;
}

/* The chart palette, defined once. A slice, its legend mark and a row avatar
   all name their colour the same way and paint with whatever --swatch it sets. */
.donut__slice--slate, .legend__mark--slate, .entry__avatar--slate { --swatch: #37474f; }
.donut__slice--red, .legend__mark--red, .entry__avatar--red { --swatch: #e0483c; }
.donut__slice--amber, .legend__mark--amber, .entry__avatar--amber { --swatch: #e9a13b; }
.donut__slice--green, .legend__mark--green, .entry__avatar--green { --swatch: #2f9e5f; }
.donut__slice--teal, .legend__mark--teal, .entry__avatar--teal { --swatch: #17948c; }
.donut__slice--blue, .legend__mark--blue, .entry__avatar--blue { --swatch: #2f7ae5; }
.donut__slice--violet, .legend__mark--violet, .entry__avatar--violet { --swatch: #7c56d6; }
.donut__slice--pink, .legend__mark--pink, .entry__avatar--pink { --swatch: #d84f8f; }

/* A slice grows from the ring's centre, not its own, so the one in focus
   steps outward along its own radius. */
.donut__slice {
  fill: none;
  stroke: var(--swatch);
  stroke-width: 5;
  stroke-linecap: butt;
  transform-box: view-box;
  transform-origin: 50% 50%;
  cursor: pointer;
  transition: opacity 200ms ease, transform 200ms var(--ease-out);
}


.chart__empty {
  margin: 24px 0;
  color: var(--ink-muted);
  font-size: 14px;
  text-align: center;
}

.legend {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}

.legend__row {
  border-bottom: 1px solid var(--line);
}

.legend__row:last-child {
  border-bottom: 0;
}

/* The whole row is the button, reaching a little past the text on each side
   so its highlight has room around the words. */
.legend__button {
  display: flex;
  align-items: center;
  gap: 10px;
  width: calc(100% + var(--space-4));
  margin: 0 calc(var(--space-2) * -1);
  padding: 7px var(--space-2);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-fast) ease, scale var(--duration-press) var(--ease-out);
}

.legend__button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.legend__mark {
  transition: opacity 200ms ease;
}

.legend__mark {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  flex: none;
  border-radius: 9px;
  background: var(--swatch);
  color: #fff;
}

.legend__mark .icon {
  width: 15px;
  height: 15px;
}


.legend__name {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
  font-size: 14px;
  line-height: 1.3;
}

.legend__amount {
  color: var(--ink-muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.legend__share {
  color: var(--ink-muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---------- Chart focus ---------- */

/* One category at a time can be in focus (the chart controller puts is-active
   on its slice, its legend row and its line in the middle, and data-active on
   the chart). Its slice steps out of the ring, the others fall back, its row
   lights up and the ring's middle names it. Tied to pointing and clicking,
   so it moves quickly: enough to show which slice is which row. */
.chart[data-active] .donut__slice:not(.is-active) {
  opacity: 0.3;
}

.donut__slice.is-active {
  transform: scale(1.07);
}

.legend__button.is-active {
  background: var(--canvas);
}

.chart[data-active] .legend__button:not(.is-active) .legend__mark {
  opacity: 0.4;
}

/* The middle of the ring: every category's line is there, stacked, and only
   the one in focus shows, so a change of focus is a crossfade in place. */
.chart__center {
  position: absolute;
  inset: 22%;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.chart__detail {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 100%;
  margin: 0;
  text-align: center;
  line-height: 1.25;
  opacity: 0;
  translate: 0 3px;
  transition: opacity 150ms ease, translate 150ms var(--ease-out);
}

.chart__detail.is-active {
  opacity: 1;
  translate: 0 0;
}

.chart__detail-name {
  max-width: 100%;
  overflow: hidden;
  color: var(--ink-muted);
  font-size: var(--text-xs);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chart__detail-amount {
  font-size: var(--text-base);
  font-weight: 600;
  white-space: nowrap;
}

.chart__detail-share {
  color: var(--ink-muted);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: no-preference) {
  .legend__button:active {
    scale: 0.98;
  }
}

/* Reduced motion keeps the fading and loses the stepping out. */
@media (prefers-reduced-motion: reduce) {
  .donut__slice.is-active {
    transform: none;
  }

  .chart__detail {
    translate: 0 0;
  }
}

/* The table decides its columns by the room the panel has, not by the window:
   beside the chart on a laptop the panel is as narrow as a tablet. */
.ledger-panel {
  container: ledger / inline-size;
}

/* The lists sit in the same card as the ledger (see Record lists). */
.ledger-panel,
.list-panel {
  --panel-pad: var(--space-5);

  min-width: 0;
  padding: var(--panel-pad);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

.ledger-panel__head {
  margin-bottom: var(--space-1);
}

/* Inside the panel the table has the panel's own border around it already,
   and its head is a soft band rather than a ruled line. */
.ledger-panel table,
.list-panel table {
  border: 0;
  border-radius: 0;
}

.ledger-panel .ledger th,
.list-panel .records th {
  border-bottom: 1px solid var(--line);
  background: var(--canvas);
}

.ledger-panel .ledger th:first-child,
.list-panel .records th:first-child {
  border-top-left-radius: 8px;
  border-bottom-left-radius: 8px;
}

.ledger-panel .ledger th:last-child,
.list-panel .records th:last-child {
  border-top-right-radius: 8px;
  border-bottom-right-radius: 8px;
}

/* ---------- Ledger rows ---------- */

.entry {
  display: flex;
  align-items: center;
  gap: 16px;
  min-width: 0;
}

.entry__avatar {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 50%;
  background: var(--swatch, var(--placeholder));
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
}


/* A logo is already a finished mark: no disc, no ring, nothing around it. It
   still keeps the 30px the initials occupy, so every description in the column
   starts at the same place. */
.entry__avatar--logo {
  border: 0;
  border-radius: 0;
  background: none;
}

/* Fills its holder, contained rather than cropped: these files are square,
   round and wide alike, and none of them should be cut to fit. */
.brand-logo {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* A one-tone logo is flattened to a silhouette and repainted in the page's own
   ink, which is the only way to reach inside an <img>. It is limited to the
   marks BrandIcon::MONOCHROME lists: doing it to a coloured logo would destroy
   the logo. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brand-logo--mono {
    filter: brightness(0) invert(1);
  }
}

:root[data-theme="dark"] .brand-logo--mono {
  filter: brightness(0) invert(1);
}

.entry__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.3;
}

/* One line each, cut with an ellipsis: every row keeps the same height, so the
   eye can run straight down the amounts. The whole description is in the
   link's title, and in the modal it opens. */
.entry__name,
.entry__meta,
.entry__where {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.entry__name {
  font-weight: 500;
  text-decoration: none;
}

.entry__name:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .entry__name:hover {
    text-decoration: underline;
  }
}

.entry__meta,
.entry__aside {
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* Only drawn once the columns it stands in for have gone (see the container
   queries at the end of the ledger rules). The account is the part that gives
   way when it is short of room; the state and the date always stay whole. */
.entry__aside {
  display: none;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.entry__where {
  min-width: 0;
}

.entry__when,
.entry__state {
  flex: none;
}

.entry__when::before {
  content: "· ";
}

.entry__aside .icon {
  width: 13px;
  height: 13px;
  vertical-align: -2px;
}

.entry__state {
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--warning-surface);
  color: var(--warning);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.5;
}

/* ---------- Row menu ---------- */

.menu--row {
  display: flex;
  justify-content: flex-end;
}

.menu__kebab {
  display: grid;
  place-items: center;
  width: var(--row-action-size);
  height: var(--row-action-size);
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
}

.menu__kebab[aria-expanded="true"] {
  border-color: var(--line);
  background: var(--canvas);
  color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .menu__kebab:hover {
    border-color: var(--line);
    background: var(--canvas);
    color: var(--ink);
  }
}

.menu__kebab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.menu--row .menu__panel {
  min-width: 160px;
}

/* ---------- Row actions ---------- */

/* Edit and Delete on every list (shared/_row_actions). Quiet until pointed at,
   so a column of them does not shout over the names, and the same size with
   their words or without. */
.row-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-1);
}

.row-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: var(--row-action-size);
  height: var(--row-action-size);
  padding: 0 9px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink-muted);
  font: inherit;
  font-size: var(--text-md);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
}

.row-action .icon {
  width: 16px;
  height: 16px;
}

.row-action:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .row-action:hover {
    border-color: var(--line);
    background: var(--canvas);
    color: var(--ink);
  }

  .row-action--danger:hover {
    border-color: transparent;
    background: var(--danger-surface);
    color: var(--danger);
  }
}

/* A finger has no hover to be warned by, so Delete turns red as it is pressed. */
.row-action--danger:active {
  background: var(--danger-surface);
  color: var(--danger);
}

@media (prefers-reduced-motion: no-preference) {
  .row-action {
    transition: transform var(--duration-press) var(--ease-out);
  }

  .row-action:active {
    transform: scale(0.97);
  }
}

/* The description takes whatever width the other columns leave, and no more:
   max-width 0 is what lets a table cell shrink below its text and cut it,
   instead of pushing the whole table wider than the page. */
.ledger__entry {
  width: 100%;
  max-width: 0;
}

.ledger__account {
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ledger__card {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
}

.ledger__card-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

.ledger__status .badge {
  white-space: nowrap;
}

/* Only the amount carries colour: a fully painted table reads as an alarm. */
.ledger__amount {
  text-align: right;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Moved, not earned or spent: the amount stays in plain ink. */
.ledger__amount.is-transfer {
  color: var(--ink-muted);
}

.entry__transfer-icon {
  width: 12px;
  height: 12px;
  margin-right: 2px;
  vertical-align: -1px;
  color: var(--brand);
}

.ledger__date {
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A faint band follows the pointer, so a wide row can be read across without
   losing the line. Only where there is a pointer to follow. */
@media (hover: hover) and (pointer: fine) {
  .ledger tbody tr:hover > td {
    background: var(--canvas);
  }
}

.ledger__actions {
  white-space: nowrap;
}

.ledger__actions form.button_to {
  display: inline;
}

.badge--paid {
  background: var(--success-surface);
  color: var(--success);
}

.badge--pending {
  background: var(--warning-surface);
  color: var(--warning);
}

.ledger__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 12px;
}

.ledger__count {
  margin: 0;
  color: var(--ink-muted);
  font-size: 13px;
}

.ledger__range::before {
  content: " · ";
}

.pagination {
  display: flex;
  gap: 8px;
}

.filters__clear {
  align-self: flex-end;
}

.filters__clear[hidden] {
  display: none;
}

.empty__title {
  margin: 0 0 4px;
  color: var(--ink);
  font-weight: 600;
}

.empty--first-run {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

/* ---------- Accounts ---------- */

.pockets {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 12px 0 0;
  padding: 10px 0 0;
  border-top: 1px dashed var(--line);
  list-style: none;
}

.pocket {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 14px;
}

.pocket--inactive {
  opacity: 0.6;
}

.pocket__name {
  color: var(--ink-muted);
  text-decoration: none;
}

.pocket__name:hover {
  color: var(--ink);
}

.pocket__balance {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}

.pocket__label {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  min-width: 0;
}

/* The pocket's own adjust button, smaller than a tile's: it sits on a line
   of text rather than on the tile's foot. */
.pocket__adjust {
  --row-action-size: 26px;
  align-self: center;
  padding: 0 5px;
}

.pocket__adjust .icon {
  width: 14px;
  height: 14px;
}

/* ---------- Earnings ---------- */

/* "100% do CDI" and what it brought this month: green, as money coming in. */
.yield {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin: 4px 0 0;
  font-size: 13px;
}

.yield__badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--success-surface);
  color: var(--success);
  font-weight: 600;
  white-space: nowrap;
}

.yield__icon {
  width: 14px;
  height: 14px;
}

.yield__earned {
  color: var(--success);
  font-variant-numeric: tabular-nums;
}

.yield--compact {
  display: inline-flex;
  margin: 0;
  font-size: 11px;
}

.yield--compact .yield__badge {
  padding: 1px 6px;
}

.yield--compact .yield__icon {
  width: 12px;
  height: 12px;
}

/* The rate and the start on the account form, shown once an index is picked. */
.yield-fields[hidden] {
  display: none;
}

.yield-fields {
  margin-top: 8px;
}

/* They come in from where the index was picked, so it reads as the choice
   opening them up. Coming back from display: none restarts the animation. */
@media (prefers-reduced-motion: no-preference) {
  .yield-fields {
    animation: yield-fields-in 200ms var(--ease-out);
  }
}

@media (prefers-reduced-motion: reduce) {
  .yield-fields {
    animation: yield-fields-fade 150ms ease-out;
  }
}

@keyframes yield-fields-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
}

@keyframes yield-fields-fade {
  from {
    opacity: 0;
  }
}

.account__total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 10px 0 0;
  color: var(--ink-muted);
  font-size: 13px;
}

.account__total strong {
  color: var(--ink);
}

.totals__split {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  margin: 0;
  color: var(--ink-muted);
  font-size: 13px;
}

.totals__split strong {
  color: var(--ink);
}

.accounts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 12px;
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
}

.account {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 16px;
}

.account--inactive {
  opacity: 0.65;
}

.account__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

/* Logo and name travel together, so the state badge still sits at the far end. */
.account__label {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.account__mark {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex: none;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  overflow: hidden;
}

.account__name {
  font-weight: 600;
  text-decoration: none;
}

.account__type,
.account__since {
  margin: 0;
  color: var(--ink-muted);
  font-size: 13px;
}

.account__balance {
  margin: 8px 0 0;
  font-size: 22px;
  font-weight: 600;
}

.account__balance--negative {
  color: var(--danger);
}

/* At the foot of the tile, so a row of tiles lines its buttons up whatever
   each one holds above them. What the tile adds (a pocket, a purchase) keeps
   its words; Edit and Delete are down to their icons at a tile's width. */
.account__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-3);
  border-top: 1px solid var(--line);
}

/* margin-top: auto leaves nothing above the rule on the tallest tile, so what
   comes last before it keeps its own distance. */
.account > :has(+ .account__actions) {
  margin-bottom: var(--space-2);
}

/* The icons line up with the text above them, not with their buttons' edges. */
.account__actions > .row-action {
  margin-left: -10px;
}

.account__actions .row-actions {
  margin-left: auto;
  margin-right: calc((var(--row-action-size) - 16px) / -2);
}

.account__actions .row-action__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.totals {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  margin-top: 16px;
}

.totals__line {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 0;
}

.totals__label {
  color: var(--ink-muted);
  font-size: 13px;
}

.totals__amount {
  font-size: 20px;
}

.empty {
  margin-top: 16px;
  padding: 32px 16px;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
  color: var(--ink-muted);
  text-align: center;
}

/* ---------- Card purchases ---------- */

/* Money that went on a card rather than out of an account wears the brand
   colour, so it reads apart from the green and red of the entries. */
.quick__icon.is-card {
  background: var(--brand-surface);
  color: var(--brand);
}

/* "Fatura out/26" keeps its lowercase month: badges capitalise every word, and
   .badge comes later in this file, so it takes both classes to win. */
.badge.badge--card {
  background: var(--brand-surface);
  color: var(--brand);
  text-transform: none;
}

.ledger__card .icon,
.entry__where .icon {
  color: var(--brand);
}

/* The purchase form's instalment preview: short rows, and a ceiling so a long
   instalment plan scrolls inside the form instead of stretching it. */
.schedule {
  display: flex;
  flex-direction: column;
  max-height: 220px;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 14px;
  list-style: none;
}

.schedule__row {
  display: grid;
  grid-template-columns: 3.5em auto 1fr;
  align-items: baseline;
  gap: 12px;
  padding: 8px 12px;
}

.schedule__row + .schedule__row {
  border-top: 1px solid var(--line);
}

.schedule__number {
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.schedule__amount {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.schedule__invoice {
  color: var(--ink-muted);
  text-align: right;
}

/* ---------- Selection ---------- */

/* The batch actions float over the bottom of the window. Opening above the
   table would push every row down just as one was ticked, and down there they
   stay within reach however far down the list the ticking went. */
.selection {
  position: fixed;
  left: 50%;
  bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  flex-wrap: wrap;
  width: min(760px, calc(100% - var(--page-gutter) * 2));
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-pop);
  translate: -50% 0;
}

.selection[hidden] {
  display: none;
}

.selection__count {
  font-size: var(--text-md);
}

.selection__clear-icon {
  display: none;
}

/* A phone: the count and a cross share the top line, and the two actions split
   the line underneath instead of stacking one per row. */
@media (max-width: 560px) {
  .selection {
    padding: var(--space-3);
  }

  .selection__count {
    flex: 1 1 auto;
  }

  .selection__clear {
    order: 1;
    display: grid;
    place-items: center;
    width: var(--target);
    height: var(--target);
    padding: 0;
  }

  .selection__clear-icon {
    display: block;
  }

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

  .selection__visible {
    order: 2;
    flex-basis: 100%;
  }

  .selection__actions {
    order: 3;
    flex-basis: 100%;
    margin-left: 0;
  }

  .selection__actions .button {
    flex: 1;
  }
}

/* It rises into place when the first row is ticked, and sinks back the same
   way, quicker, once the selection is cleared: the path says where it lives.
   Reduced motion keeps the fade and loses the movement. */
.selection {
  transition: opacity var(--duration-fast) var(--ease-out), display var(--duration-fast) allow-discrete;
}

.selection[hidden] {
  opacity: 0;
  transition-duration: 120ms;
}

@starting-style {
  .selection {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .selection {
    transition: opacity var(--duration-fast) var(--ease-out), translate var(--duration-fast) var(--ease-out),
      display var(--duration-fast) allow-discrete;
  }

  .selection[hidden] {
    translate: -50% 12px;
    transition-duration: 120ms;
  }

  @starting-style {
    .selection {
      translate: -50% 12px;
    }
  }
}

/* While it is up, the page keeps room below the list so the last rows can
   scroll clear of it, and anything scrolled into view stops above it. */
html:has(.selection:not([hidden])) {
  scroll-padding-bottom: 128px;
}

body:has(.selection:not([hidden])) .main {
  padding-bottom: 136px;
}

.selection__visible {
  color: var(--ink-muted);
  font-size: 13px;
}

.selection__visible[hidden] {
  display: none;
}

.selection__actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-left: auto;
}

/* Compact so a row of them fits: these are actions, not a form to fill in. */
.field__input--compact {
  padding: 6px 8px;
  font-size: 13px;
}

.ledger__select {
  width: var(--target);
  padding-right: 0;
}

/* The box stays a box; the label around it is the target, so a finger does
   not have to find 16 pixels. */
.ledger__tick {
  display: grid;
  place-items: center;
  width: var(--target);
  height: var(--target);
  margin: calc(var(--space-2) * -1) 0;
  cursor: pointer;
}

.ledger__checkbox {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* ---------- Tags ---------- */

.tag {
  display: inline-block;
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  font-size: 12px;
  white-space: nowrap;
}

.tag--muted {
  opacity: 0.55;
}

.tag--more {
  --tag-surface: var(--canvas);
  --tag-ink: var(--ink-muted);
}

/* Each colour carries two tones: the pale one is the chip's background, the
   strong one its text — and the strong one is what a swatch is filled with,
   because a pastel dot is not a colour anybody can tell apart.

   The dark theme keeps the same idea upside down: the chip is a deep tint of
   its colour and the text a bright one, because a pastel chip would glare on a
   dark page. */
.tag--slate { --tag-surface: light-dark(#eceff1, #262d31); --tag-ink: light-dark(#37474f, #b0c1cb); }
.tag--red { --tag-surface: light-dark(#fdecea, #391512); --tag-ink: light-dark(#b3261e, #f19b94); }
.tag--amber { --tag-surface: light-dark(#fff4e5, #33240c); --tag-ink: light-dark(#8a5300, #e6b366); }
.tag--green { --tag-surface: light-dark(#eaf5ee, #10281a); --tag-ink: light-dark(#1e6b3a, #79c797); }
.tag--teal { --tag-surface: light-dark(#e2f3f2, #0e2827); --tag-ink: light-dark(#10615c, #6cc3bd); }
.tag--blue { --tag-surface: light-dark(#e8f0fe, #131f38); --tag-ink: light-dark(#17458f, #9ab6f0); }
.tag--violet { --tag-surface: light-dark(#f0eaf8, #211732); --tag-ink: light-dark(#55308d, #b79ae2); }
.tag--pink { --tag-surface: light-dark(#fdeaf3, #331324); --tag-ink: light-dark(#98285e, #eb95bd); }

.tag[class*="tag--"] {
  background: var(--tag-surface, var(--canvas));
  color: var(--tag-ink, var(--ink-muted));
}

.ledger__tags {
  max-width: 220px;
  white-space: nowrap;
}

.tag-picker,
.colour-picker {
  border: 0;
  padding: 0;
  margin: 0;
}

.tag-picker__grid {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 10px;
}

.tag-picker__input,
.colour-picker__input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.tag-picker__label {
  display: inline-block;
  padding: 2px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  cursor: pointer;
  opacity: 0.5;
  transition: opacity var(--duration-fast) ease, border-color var(--duration-fast) ease,
    scale var(--duration-press) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .tag-picker__label:hover {
    opacity: 0.8;
  }
}

.tag-picker__input:checked + .tag-picker__label {
  border-color: var(--accent);
  opacity: 1;
}

.tag-picker__input:focus-visible + .tag-picker__label,
.colour-picker__input:focus-visible + .colour-picker__label {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.colour-picker__grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 6px;
}

.colour-picker__label {
  display: block;
  width: 28px;
  height: 28px;
  border: 2px solid transparent;
  border-radius: 50%;
  background: var(--tag-ink);
  cursor: pointer;
  transition: border-color var(--duration-fast) ease, box-shadow var(--duration-fast) ease,
    scale var(--duration-fast) var(--ease-out);
}

/* The swatch under the pointer grows a touch. Only where there is a pointer:
   on a touch screen the hover would stay stuck to whichever was tapped. */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .colour-picker__label:hover {
    scale: 1.08;
  }

  /* A press under the pointer still sinks, rather than staying grown. */
  .colour-picker__label:active {
    scale: 0.95;
  }
}

/* A ring outside the swatch, so the colour itself is never covered up. */
.colour-picker__input:checked + .colour-picker__label {
  border-color: var(--surface);
  box-shadow: 0 0 0 2px var(--accent);
}



/* ---------- Icon picker ---------- */

.icon-picker {
  border: 0;
  padding: 0;
  margin: 0;
}

.icon-picker__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
  gap: 8px;
  margin-top: 6px;
}

.icon-picker__input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.icon-picker__label {
  display: grid;
  place-items: center;
  height: 44px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  color: var(--ink-muted);
  cursor: pointer;
  transition: border-color var(--duration-fast) ease, box-shadow var(--duration-fast) ease,
    color var(--duration-fast) ease, scale var(--duration-press) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .icon-picker__label:hover {
    border-color: var(--line-strong);
    color: var(--ink);
  }
}

.icon-picker__label .icon {
  width: 22px;
  height: 22px;
}

.icon-picker__none {
  font-size: 20px;
  line-height: 1;
}

.icon-picker__input:checked + .icon-picker__label {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--ink);
}

.icon-picker__input:focus-visible + .icon-picker__label {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* A subcategory is one step in from its parent, and says whose it is. */
.category--child td:first-child {
  padding-left: 32px;
}

.category__parent {
  color: var(--ink-muted);
  font-size: 13px;
}

.table__empty {
  padding: 24px 14px;
  color: var(--ink-muted);
  text-align: center;
}

dt {
  color: var(--ink-muted);
  font-size: 13px;
}

dd {
  margin: 0 0 12px;
}

.badge {
  display: inline-block;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: 12px;
  text-transform: capitalize;
}

.badge--active {
  background: var(--success-surface);
  color: var(--success);
}

.badge--inactive {
  background: var(--danger-surface);
  color: var(--danger);
}

.badge--expense {
  background: var(--danger-surface);
  color: var(--danger);
}

.badge--income {
  background: var(--success-surface);
  color: var(--success);
}

/* It arrives with the refused form, so it eases in a few pixels from above:
   the one thing on the form that is new. */
.errors {
  padding: 10px 14px;
  border-radius: 8px;
  background: var(--danger-surface);
  color: var(--danger);
  font-size: 14px;
  transition: opacity 200ms var(--ease-out), translate 200ms var(--ease-out);
}

@starting-style {
  .errors {
    opacity: 0;
    translate: 0 -4px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .errors {
    transition-property: opacity;
  }
}

.errors ul {
  margin: 0;
  padding-left: 1.2em;
}

/* One problem needs no bullet. */
.errors ul:has(> li:only-child) {
  padding-left: 0;
  list-style: none;
}

/* ---------- Sign in ---------- */

/* The screen seen before any other, once in a while: it can afford a little
   atmosphere, as long as the card stays the only thing there is to read. The
   card is solid, so the text on it never depends on what drifts behind. */
.auth {
  /* The glows: the brand's blue, a violet and a teal, faint enough in the
     light theme to leave the page white, deeper in the dark one. */
  --auth-glow-one: color-mix(in srgb, var(--brand) 26%, transparent);
  --auth-glow-two: light-dark(rgb(139 92 246 / 18%), rgb(139 92 246 / 24%));
  --auth-glow-three: light-dark(rgb(20 184 166 / 16%), rgb(20 184 166 / 18%));
  --auth-dot: light-dark(rgb(0 0 0 / 10%), rgb(255 255 255 / 8%));

  position: relative;
  isolation: isolate;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-6);
  padding: calc(var(--space-8) + env(safe-area-inset-top, 0px)) var(--gutter-right)
    calc(var(--space-8) + env(safe-area-inset-bottom, 0px)) var(--gutter-left);
}

/* Behind everything, fixed to the window so it neither stretches the page
   nor scrolls with it, and contained so nothing it does reaches the layout. */
.auth__backdrop {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  contain: strict;
}

/* A grid of dots, still, that fades out towards the edges: texture under the
   card rather than a pattern to look at. */
.auth__backdrop::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(var(--auth-dot) 1px, transparent 1.5px);
  background-size: 22px 22px;
  mask-image: radial-gradient(ellipse 65% 55% at 50% 45%, #000 15%, transparent 75%);
}

/* Each glow is a radial gradient rather than a blurred shape: it is painted
   once, as its own layer, and after that only moved and scaled, which the
   compositor does without repainting anything. The drifts are long and out
   of step, so the light never visibly repeats and nothing ever pulls at the
   eye. */
/* Each is centred on the point it is placed at, and capped in size: a layer
   is kept in memory at the screen's own resolution, and past a thousand
   pixels a bigger glow only costs more without looking any different. Its
   drift is measured in its own size, so it travels alike on any screen. */
.auth__glow {
  position: absolute;
  width: min(70vmax, 1000px);
  aspect-ratio: 1;
  border-radius: 50%;
  translate: -50% -50%;
  will-change: transform;
}

.auth__glow--one {
  top: 8%;
  left: 12%;
  background: radial-gradient(closest-side, var(--auth-glow-one), transparent);
  animation: auth-drift-one 38s ease-in-out infinite alternate;
}

.auth__glow--two {
  top: 96%;
  left: 84%;
  background: radial-gradient(closest-side, var(--auth-glow-two), transparent);
  animation: auth-drift-two 46s ease-in-out -14s infinite alternate;
}

.auth__glow--three {
  top: 36%;
  left: 100%;
  background: radial-gradient(closest-side, var(--auth-glow-three), transparent);
  animation: auth-drift-three 42s ease-in-out -26s infinite alternate;
}

@keyframes auth-drift-one {
  to { transform: translate3d(22%, 18%, 0) scale(1.12); }
}

@keyframes auth-drift-two {
  to { transform: translate3d(-24%, -16%, 0) scale(1.08); }
}

@keyframes auth-drift-three {
  to { transform: translate3d(-20%, 24%, 0) scale(1.15); }
}

/* A phone gets two glows: the same light for less work. */
@media (max-width: 560px) {
  .auth__glow--three {
    display: none;
  }
}

/* The name above the card, with the app's mark in the brand's blue. */
.auth__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  color: var(--ink);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.auth__mark {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  background: var(--brand);
  box-shadow: 0 6px 16px color-mix(in srgb, var(--brand) 35%, transparent);
  color: var(--accent-ink);
}

.auth__mark .icon {
  width: 20px;
  height: 20px;
}

/* Lifted further than a card on a page, since it floats over light. */
.auth__card {
  width: 100%;
  max-width: 400px;
  padding: var(--space-8);
  box-shadow: var(--shadow-pop);
}

.auth__header {
  margin-bottom: var(--space-6);
  text-align: center;
}

.auth__title {
  margin: 0;
  font-size: var(--text-xl);
  font-weight: 600;
  letter-spacing: -0.02em;
}

/* Balanced, so a line that wraps on a phone does not leave one word alone. */
.auth__subtitle {
  margin: var(--space-1) 0 0;
  color: var(--ink-muted);
  font-size: var(--text-md);
  text-wrap: balance;
}

/* What the last attempt came back with, inside the card, above the fields it
   is about. It eases in a few pixels, being the one new thing there. */
.auth__message {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: calc(var(--space-2) * -1) 0 var(--space-5);
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  font-size: var(--text-md);
  line-height: 1.4;
  transition: opacity 200ms var(--ease-out), translate 200ms var(--ease-out);
}

@starting-style {
  .auth__message {
    opacity: 0;
    translate: 0 -4px;
  }
}

.auth__message .icon {
  width: 18px;
  height: 18px;
  margin-top: 1px;
}

.auth__message--error {
  background: var(--danger-surface);
  color: var(--danger);
}

.auth__message--notice {
  background: var(--success-surface);
  color: var(--success);
}

/* Fields answer the pointer before they are clicked. */
.auth .field__input {
  transition: border-color var(--duration-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .auth .field__input:hover:not(:focus) {
    border-color: var(--ink-muted);
  }
}

.auth__submit {
  width: 100%;
  margin-top: var(--space-1);
}

/* While the form is on its way (Turbo marks it aria-busy, disables the
   button and swaps in data-turbo-submits-with), a spinner turns beside the
   words: the button is working, not unavailable, so it keeps its colour. */
.auth__form[aria-busy="true"] .auth__submit::before {
  content: "";
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: auth-spin 700ms linear infinite;
}

.auth__submit:disabled {
  cursor: progress;
}

@keyframes auth-spin {
  to { rotate: 1turn; }
}

/* The brand and the card rise into place as the page opens, the card a beat
   behind. Not when the page comes back with a message: then it was already
   there, and a refusal gives the card a short shake instead, the way a
   sign-in window says no. */
.auth__brand,
.auth__card {
  transition: opacity 450ms var(--ease-out), translate 450ms var(--ease-out);
}

.auth__card {
  transition-delay: 60ms;
}

@starting-style {
  .auth__brand,
  .auth__card {
    opacity: 0;
    translate: 0 10px;
  }
}

.auth--settled .auth__brand,
.auth--settled .auth__card {
  transition: none;
}

.auth--refused .auth__card {
  animation: auth-shake 380ms var(--ease-out);
}

@keyframes auth-shake {
  20% { translate: -6px 0; }
  40% { translate: 5px 0; }
  60% { translate: -3px 0; }
  80% { translate: 2px 0; }
}

/* Reduced motion: the light stays where it is, things fade in without
   rising, a refusal does not shake, and the spinner turns slower. */
@media (prefers-reduced-motion: reduce) {
  .auth__glow {
    animation: none;
  }

  .auth__brand,
  .auth__card {
    transition-property: opacity;
  }

  .auth__message {
    transition-property: opacity;
  }

  .auth--refused .auth__card {
    animation: none;
  }

  .auth__form[aria-busy="true"] .auth__submit::before {
    animation-duration: 1.6s;
  }
}

@media (max-width: 560px) {
  .auth__card {
    padding: var(--space-6);
  }
}

.form {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
}

.card__title {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
}

.card__note {
  margin: 2px 0 0;
  color: var(--ink-muted);
  font-size: 13px;
}

/* ---------- Presentation ---------- */

/* The public page about the app (/sobre), reached from a link outside it and
   most often on a phone. It stands on its own, without the app's nav. One
   column, read top to bottom: what it is, the film as wide as the column,
   then what it does. The scenery is the sign-in screen's light, held still:
   here it is a backdrop to read over, not a moment to look at. */
.showcase {
  position: relative;
  isolation: isolate;
  overflow: clip;
  padding: calc(var(--space-8) + env(safe-area-inset-top, 0px)) var(--gutter-right)
    calc(56px + env(safe-area-inset-bottom, 0px)) var(--gutter-left);
}

.showcase > :not(.showcase__backdrop) {
  max-width: 1040px;
  margin-inline: auto;
}

.showcase__backdrop {
  position: absolute;
  inset: 0 0 auto;
  z-index: -1;
  height: 760px;
  pointer-events: none;
  background:
    radial-gradient(closest-side, color-mix(in srgb, var(--brand) 22%, transparent), transparent) -12% -40% / 70% 120% no-repeat,
    radial-gradient(closest-side, light-dark(rgb(139 92 246 / 14%), rgb(139 92 246 / 22%)), transparent) 112% -10% / 60% 100% no-repeat,
    radial-gradient(closest-side, light-dark(rgb(20 184 166 / 12%), rgb(20 184 166 / 16%)), transparent) 50% 120% / 80% 70% no-repeat;
  mask-image: linear-gradient(#000 55%, transparent);
}

.showcase__hero {
  padding-block: var(--space-4) var(--space-8);
  text-align: center;
}

/* The name with the app's mark, as above the sign-in card. */
.showcase__brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.showcase__mark {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  background: var(--brand);
  box-shadow: 0 6px 16px color-mix(in srgb, var(--brand) 35%, transparent);
  color: var(--accent-ink);
}

.showcase__mark .icon {
  width: 20px;
  height: 20px;
}

.showcase__title {
  max-width: 17ch;
  margin: var(--space-5) auto 0;
  font-size: clamp(32px, 6.4vw, 54px);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.03em;
  text-wrap: balance;
}

.showcase__lead {
  max-width: 62ch;
  margin: var(--space-4) auto 0;
  color: var(--ink-muted);
  font-size: clamp(16px, 2vw, 18px);
  line-height: 1.6;
  text-wrap: pretty;
}

.showcase__film {
  margin-block: 0;
}

/* The width and height on the tag reserve the frame before the file is in,
   so nothing below jumps when the poster arrives. */
.showcase__video {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  box-shadow: var(--shadow-pop);
}

.showcase__section {
  margin-top: 56px;
}

.showcase__heading {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: 700;
  letter-spacing: -0.02em;
}

.showcase__note,
.showcase__more {
  max-width: 72ch;
  margin: var(--space-2) 0 0;
  color: var(--ink-muted);
  line-height: 1.6;
}

.showcase__more {
  margin-top: var(--space-5);
}

.features {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
  gap: var(--space-4);
  margin: var(--space-6) 0 0;
  padding: 0;
  list-style: none;
}

.feature {
  padding: var(--space-5);
}

.feature__icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius);
  background: var(--brand-surface);
  color: var(--brand);
}

.feature__icon .icon {
  width: 22px;
  height: 22px;
}

.feature__title {
  margin: var(--space-4) 0 0;
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.feature__text {
  margin: var(--space-2) 0 0;
  color: var(--ink-muted);
  line-height: 1.55;
}

/* What is coming looks unfinished on purpose: a dashed outline instead of a
   card, and its icon in plain ink rather than the brand's blue, so it never
   reads as one of the features above. */
.upcoming {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
  gap: var(--space-4);
  margin: var(--space-6) 0 0;
  padding: 0;
  list-style: none;
}

.upcoming__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-card);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.3;
}

.upcoming__icon {
  display: grid;
  flex: none;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius);
  background: var(--canvas);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--ink-muted);
}

.upcoming__icon .icon {
  width: 22px;
  height: 22px;
}

.showcase__footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-top: 56px;
  padding-top: var(--space-6);
  border-top: 1px solid var(--line);
  color: var(--ink-muted);
  font-size: var(--text-md);
}

.showcase__footer p {
  margin: 0;
}

.showcase__social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Full-size targets for a thumb, under the footer's quiet look. */
.showcase__social-link {
  min-height: var(--target-touch);
  padding-inline: 16px;
}

.showcase__social-link .icon {
  width: 18px;
  height: 18px;
}

/* The page arrives in reading order: the name, the headline, the words under
   it, then the film, each a beat behind the one before. */
.showcase__hero > *,
.showcase__film {
  transition: opacity 450ms var(--ease-out), translate 450ms var(--ease-out);
}

.showcase__title {
  transition-delay: 50ms;
}

.showcase__lead {
  transition-delay: 100ms;
}

.showcase__film {
  transition-delay: 170ms;
}

@starting-style {
  .showcase__hero > *,
  .showcase__film {
    opacity: 0;
    translate: 0 10px;
  }
}

/* Each feature, and each thing coming, rises a little as it scrolls into
   view, where the browser can tie the motion to the scroll itself; elsewhere
   they are just there. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .feature,
    .upcoming__item {
      animation-name: showcase-rise;
      animation-timing-function: var(--ease-out);
      animation-fill-mode: both;
      animation-timeline: view();
      animation-range: entry 0% entry 70%;
    }
  }
}

@keyframes showcase-rise {
  from {
    opacity: 0;
    translate: 0 16px;
  }
}

/* Reduced motion: the page fades in without rising, and the rest holds still. */
@media (prefers-reduced-motion: reduce) {
  .showcase__hero > *,
  .showcase__film {
    transition-property: opacity;
  }
}

/* ---------- Brand vote ---------- */

/* The public vote on the brand's name (/votacao), on the same open page as
   /sobre: the question, then the three mascots side by side (stacked on a
   phone), then who is voting. Each mascot brings its own colour, taken from
   its drawings, and paints its stage, its pick and its bar in the count with
   it; the pages around them stay in the app's own ink. */
.mascot--nori {
  --mascot: light-dark(#d65a20, #f08a55);
  --mascot-tint: light-dark(#fdebe0, #2f1e16);
}

.mascot--lumi {
  --mascot: light-dark(#13878f, #4cc3c9);
  --mascot-tint: light-dark(#dff2f2, #132a2b);
}

.mascot--tuki {
  --mascot: light-dark(#4f7f57, #8dbb93);
  --mascot-tint: light-dark(#e5efe2, #1a271b);
}

/* "Votação aberta", with a dot that breathes like a live signal. */
.ballot__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: 6px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--text-sm);
  font-weight: 500;
}

.ballot__pulse {
  position: relative;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--success);
}

.ballot__pulse::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: inherit;
  opacity: 0;
}

.ballot__form {
  display: flex;
  flex-direction: column;
  gap: 40px;
}

.mascots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  gap: var(--space-4);
  margin-top: var(--space-6);
}

.mascot {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  overflow: clip;
  transition: border-color 200ms var(--ease-out), box-shadow 200ms var(--ease-out);
}

/* The pick: the card takes its mascot's colour, all the way round. */
.mascot:has(.mascot__radio:checked) {
  border-color: var(--mascot);
  box-shadow: 0 0 0 1px var(--mascot), 0 14px 34px color-mix(in srgb, var(--mascot) 24%, transparent);
}

.mascot:has(.mascot__radio:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Out of sight, still in the tab order and still read out. */
.mascot__radio {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  opacity: 0;
}

.mascot__choice {
  position: relative;
  display: block;
  cursor: pointer;
}

/* Feet on the floor: a full figure stands on the stage's foot, scaled down to
   fit; a face peeks up from it at its own size, so it reads as a close-up. */
.mascot__stage {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  height: clamp(220px, 56vw, 270px);
  padding: var(--space-5) var(--space-4) 0;
  background:
    radial-gradient(closest-side, color-mix(in srgb, var(--mascot) 16%, transparent), transparent) 50% 112% / 90% 46% no-repeat,
    var(--mascot-tint);
}

.mascot__figure {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  transform-origin: 50% 100%;
  transition: translate 220ms var(--ease-out);
}

.mascot__caption {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
  padding: var(--space-4) var(--space-5) 0;
}

.mascot__name {
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.mascot__kind {
  color: var(--ink-muted);
  font-size: var(--text-md);
}

/* The radio's own face: an empty ring, filled with a tick once picked. */
.mascot__tick {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 2px solid var(--line-strong);
  border-radius: 50%;
  background: var(--surface);
  color: var(--surface);
  transition: background-color 200ms var(--ease-out), border-color 200ms var(--ease-out);
}

.mascot__tick .icon {
  width: 16px;
  height: 16px;
  stroke-width: 3;
  opacity: 0;
  scale: 0.4;
  transition: opacity 160ms var(--ease-out), scale 260ms var(--ease-out);
}

.mascot:has(.mascot__radio:checked) .mascot__tick {
  border-color: var(--mascot);
  background: var(--mascot);
}

.mascot:has(.mascot__radio:checked) .mascot__tick .icon {
  opacity: 1;
  scale: 1;
}

@media (hover: hover) and (pointer: fine) {
  .mascot__choice:hover .mascot__tick {
    border-color: var(--mascot);
  }

  .mascot__choice:hover .mascot__figure {
    translate: 0 -4px;
  }
}

/* Every drawing of the mascot, small: the one on the stage is marked. */
.mascot__poses {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
  gap: 6px;
  padding: var(--space-3) var(--space-4) var(--space-4);
}

.mascot__pose {
  display: block;
  padding: 3px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: var(--canvas);
  cursor: pointer;
  transition: scale var(--duration-press) var(--ease-out), border-color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}

.mascot__pose[aria-pressed="true"] {
  border-color: var(--mascot);
  background: var(--mascot-tint);
}

.mascot__pose:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.mascot__thumb {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: contain;
  object-position: 50% 100%;
}

@media (hover: hover) and (pointer: fine) {
  .mascot__pose:hover {
    border-color: var(--line-strong);
  }

  .mascot__pose[aria-pressed="true"]:hover {
    border-color: var(--mascot);
  }
}

/* Who is voting, under the mascots and narrower than them. */
.ballot__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  width: 100%;
  max-width: 560px;
  margin-inline: auto;
  padding: var(--space-6);
}

.ballot__card-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
}

.ballot__optional {
  color: var(--ink-muted);
}

.ballot__submit {
  min-height: var(--target-touch);
  margin-top: var(--space-1);
  font-weight: 600;
}

/* After the vote: the mascot it went to, cheering in a circle of its colour. */
.thanks__stage {
  display: grid;
  place-items: end center;
  width: 176px;
  height: 176px;
  margin: 0 auto var(--space-2);
  border-radius: 50%;
  background: var(--mascot-tint);
  box-shadow: 0 0 0 10px color-mix(in srgb, var(--mascot-tint) 45%, transparent);
  overflow: clip;
}

.thanks__figure {
  display: block;
  width: 88%;
  height: auto;
}

.showcase__title.thanks__title {
  max-width: 20ch;
}

.thanks strong {
  color: var(--ink);
  text-decoration: underline 3px var(--mascot);
  text-underline-offset: 5px;
}

.thanks q {
  color: var(--ink);
  font-weight: 600;
}

/* Narrower than the page's column: three rows of bars read better short. */
.showcase > .ballot__standing {
  max-width: 720px;
}

/* The count: one row per mascot, its face, its votes and a bar of its share. */
.tally {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: var(--space-5) 0 0;
  padding: 0;
  list-style: none;
}

.tally__row {
  display: grid;
  grid-template-columns: 52px 1fr;
  align-items: center;
  gap: 14px;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
}

.tally__face {
  display: block;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--mascot-tint);
  object-fit: contain;
  object-position: 50% 100%;
}

.tally__head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin: 0;
}

.tally__name {
  font-size: 17px;
  font-weight: 700;
}

.tally__badge {
  padding: 1px 8px;
  border: 1px solid var(--mascot);
  border-radius: var(--radius-pill);
  background: var(--mascot-tint);
  font-size: var(--text-xs);
  font-weight: 600;
}

.tally__count {
  margin-left: auto;
  color: var(--ink-muted);
  font-size: var(--text-md);
  font-variant-numeric: tabular-nums;
}

.tally__row.is-leading .tally__count {
  color: var(--ink);
  font-weight: 600;
}

.tally__bar {
  display: block;
  height: 10px;
  margin-top: var(--space-2);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--line) 70%, transparent);
  overflow: hidden;
}

.tally__fill {
  display: block;
  width: var(--share);
  height: 100%;
  border-radius: inherit;
  background: var(--mascot);
  transform-origin: 0 50%;
}

.ballot__admin {
  margin-top: 48px;
  text-align: center;
}

/* Admins' side (/votacao/resultados): the count and the suggestions in two
   panels over the list of every vote. */
.vote-results {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
  gap: var(--space-4);
  margin-top: var(--space-5);
}

.vote-results__panel {
  padding: var(--space-5);
}

.vote-results .tally__row {
  padding: 0;
  border: 0;
}

.vote-suggestions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

.vote-suggestion {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--canvas);
}

.vote-suggestion__count {
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

.records td.vote-results__suggestion {
  white-space: normal;
}

/* Motion. The cards arrive one after another under the question; a pick makes
   its mascot hop where it stands, squashing into the jump and landing; a pose
   picked below grows onto the stage (pose-gallery controller); after the vote
   the mascot pops into its circle and the bars grow to their share. The dot
   by "Votação aberta" keeps breathing: the vote is live. */
@media (prefers-reduced-motion: no-preference) {
  .mascot {
    animation: ballot-rise 500ms var(--ease-out) both;
    animation-delay: calc(180ms + var(--i, 0) * 80ms);
  }

  .mascot:has(.mascot__radio:checked) .mascot__figure {
    animation: mascot-hop 560ms var(--ease-out);
  }

  .mascot__pose:active {
    scale: 0.94;
  }

  .thanks__figure {
    animation: thanks-pop 620ms var(--ease-out) 120ms both;
    transform-origin: 50% 100%;
  }

  .tally__fill {
    animation: tally-grow 700ms var(--ease-out) both;
    animation-delay: calc(260ms + var(--i, 0) * 90ms);
  }

  .ballot__pulse::after {
    animation: ballot-pulse 2.4s var(--ease-out) infinite;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mascot,
  .thanks__figure {
    animation: ballot-fade 400ms ease both;
  }
}

@keyframes ballot-rise {
  from {
    opacity: 0;
    translate: 0 16px;
  }
}

@keyframes ballot-fade {
  from {
    opacity: 0;
  }
}

@keyframes mascot-hop {
  0% {
    translate: 0 0;
    scale: 1;
  }

  16% {
    translate: 0 0;
    scale: 1.05 0.93;
  }

  44% {
    translate: 0 -16px;
    scale: 0.97 1.04;
  }

  72% {
    translate: 0 0;
    scale: 1.03 0.97;
  }

  100% {
    translate: 0 0;
    scale: 1;
  }
}

@keyframes thanks-pop {
  from {
    opacity: 0;
    translate: 0 24px;
    scale: 0.7;
  }

  60% {
    opacity: 1;
    scale: 1.04;
  }

  to {
    translate: 0 0;
    scale: 1;
  }
}

@keyframes tally-grow {
  from {
    scale: 0 1;
  }
}

@keyframes ballot-pulse {
  from {
    opacity: 0.55;
    scale: 1;
  }

  to {
    opacity: 0;
    scale: 3;
  }
}

/* ---------- Forms ---------- */

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* A field the form hides for now (a transfer has no category) stays hidden,
   whatever display the rule above gives it. */
.field[hidden] {
  display: none;
}

.field__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.field__label {
  color: var(--ink-muted);
  font-size: 13px;
}

.field__input-wrap {
  position: relative;
}

.field__input {
  width: 100%;
  padding: 11px 12px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  font: inherit;
}

/* iOS zooms into any field under 16px when it takes focus and never zooms back
   out. Kept above the size modifiers, so the big amount field stays big. */
@media (pointer: coarse) {
  .field__input {
    font-size: 16px;
  }
}

/* A field the server refused (aria-invalid comes from field_error_proc) is
   drawn in the colour of the message that names it. */
.field__input[aria-invalid="true"] {
  border-color: var(--danger);
}

/* Shown for reading, plainly not for typing into. */
.field__input:disabled {
  background: var(--canvas);
  color: var(--ink-muted);
  cursor: not-allowed;
}

.field__input--with-action {
  padding-right: 44px;
}

/* Both modifiers must stay after .field__input: its padding shorthand would
   otherwise win on equal specificity and reset the side they widen. */
.field__input--with-icon {
  padding-left: 38px;
}

.field__input:focus-visible,
.field__toggle:focus-visible,
.button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.field__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: 50%;
  right: 10px;
  width: 28px;
  height: 28px;
  transform: translateY(-50%);
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
}

.field__toggle:hover {
  color: var(--ink);
}

.field__toggle [data-password-icon="visible"],
.is-password-visible .field__toggle [data-password-icon="masked"] {
  display: none;
}

.is-password-visible .field__toggle [data-password-icon="visible"] {
  display: block;
}

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

/* ---------- Password strength ---------- */

.strength {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 4px;
}

/* An explicit display beats the hidden attribute, so put it back. */
.strength[hidden],
.strength__nudge[hidden] {
  display: none;
}

/* The panel appears with the first letter typed and pushes the fields under
   it down, so it eases in instead of jumping, and out the same way when the
   field is cleared. */
.strength {
  transition: opacity 200ms var(--ease-out), translate 200ms var(--ease-out), display 200ms allow-discrete;
}

.strength[hidden] {
  opacity: 0;
  translate: 0 -4px;
  transition-duration: 120ms;
}

@starting-style {
  .strength {
    opacity: 0;
    translate: 0 -4px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .strength,
  .strength[hidden] {
    translate: 0 0;
  }

  @starting-style {
    .strength {
      translate: 0 0;
    }
  }
}

.strength__meter {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
}

/* Each bar takes its colour over a moment, so the meter reads as filling up
   (or draining) as the password changes, not as flickering. */
.strength__meter span {
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--line);
  transition: background-color 200ms ease;
}

.strength__meter[data-level="weak"] span:nth-child(1) {
  background: var(--danger);
}

.strength__meter[data-level="medium"] span:nth-child(-n + 2) {
  background: var(--warning);
}

.strength__meter[data-level="strong"] span {
  background: var(--success);
}

.strength__level,
.strength__nudge {
  margin: 0;
  color: var(--ink-muted);
  font-size: 13px;
}

.strength__rules {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 13px;
  color: var(--ink-muted);
}

.strength__rule {
  display: flex;
  align-items: center;
  gap: 8px;
}

.strength__rule {
  transition: color var(--duration-fast) ease;
}

.strength__rule.is-met {
  color: var(--success);
}

.strength__mark {
  width: 14px;
  height: 14px;
  flex: none;
  border: 1px solid currentColor;
  border-radius: 50%;
  transition: background-color var(--duration-fast) ease, border-color var(--duration-fast) ease;
}

.strength__rule.is-met .strength__mark {
  border-color: var(--success);
  background: var(--success) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3.5 7.4l2.3 2.3 4.7-5' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
}

/* A link dressed as a button is a button: no underline from being an <a>, and
   its words centred however tall the row it sits in stretches it. */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 11px 20px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font: inherit;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
}

/* A press is answered the moment the finger lands, not when it lifts. */
@media (prefers-reduced-motion: no-preference) {
  .button {
    transition: transform var(--duration-press) var(--ease-out);
  }

  .button:active {
    transform: scale(0.97);
  }
}

.button--primary {
  background: var(--accent);
  color: var(--accent-ink);
}

.button--primary:hover {
  background: var(--accent-hover);
}

.button--block {
  display: block;
  width: 100%;
  margin-top: 4px;
}

.button--quiet {
  padding: 6px 14px;
  border-color: var(--line-strong);
  background: var(--surface);
  font-size: 14px;
}

.button--quiet:hover {
  border-color: var(--ink);
}

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

.button--danger:hover {
  border-color: var(--danger);
  background: var(--danger);
  color: var(--accent-ink);
}

/* ---------- Confirm dialog ---------- */

.modal {
  width: 100%;
  max-width: 380px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
}

.modal::backdrop {
  background: var(--overlay);
}

.modal--form {
  max-width: 520px;
}

.modal {
  overscroll-behavior: contain;
}

/* Under a finger, dragging outside an open dialog would otherwise scroll the
   page behind it. With a mouse the page is left alone: hiding its scrollbar
   would shift it sideways. */
@media (pointer: coarse) {
  html:has(dialog[open]) {
    overflow: hidden;
  }
}

/* The dialog and its dimming fade in together, and leaving is quicker than
   arriving: the reader asked for it to go. Discrete transitions keep the
   closing half from being cut off by display: none. A fade is kept even for
   reduced motion; the movement is what goes. */
.modal,
.modal::backdrop {
  opacity: 0;
  transition: opacity 140ms var(--ease-out), overlay 140ms allow-discrete, display 140ms allow-discrete;
}

.modal[open],
.modal[open]::backdrop {
  opacity: 1;
  transition-duration: 200ms;
}

@starting-style {
  .modal[open],
  .modal[open]::backdrop {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: no-preference) {
  /* A centred dialog rises the last few pixels into place. */
  .modal {
    translate: 0 8px;
    transition: opacity 140ms var(--ease-out), translate 140ms var(--ease-out),
      overlay 140ms allow-discrete, display 140ms allow-discrete;
  }

  .modal[open] {
    translate: 0 0;
    transition-duration: 200ms;
  }

  @starting-style {
    .modal[open] {
      translate: 0 8px;
    }
  }
}

.modal__card {
  padding: 24px;
}

.modal__title {
  margin: 0 0 8px;
  font-size: 18px;
  font-weight: 600;
  line-height: 1.3;
}

.modal__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 16px;
}

.modal__header .modal__title {
  margin: 0;
}

/* The size of a row's buttons, 44px under a finger. The negative margins keep
   the header as tall as its title, centre the button on the title's first
   line, and line the icon, not the button's edge, up with the padding. */
.modal__close {
  display: grid;
  place-items: center;
  flex: none;
  width: var(--row-action-size);
  height: var(--row-action-size);
  margin-block: calc((var(--row-action-size) - 24px) / -2);
  margin-right: calc((var(--row-action-size) - 18px) / -2);
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
}

.modal__close .icon {
  width: 18px;
  height: 18px;
}

.modal__close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .modal__close:hover {
    background: var(--canvas);
    color: var(--ink);
  }
}

/* The frame is only a landing spot; the dialog inside it does the showing. */
turbo-frame#modal {
  display: contents;
}

.modal__message {
  margin: 0;
  color: var(--ink-muted);
  font-size: 14px;
}

.modal__actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 24px;
}

.modal__actions .button--primary.button--danger {
  border-color: var(--danger);
  background: var(--danger);
  color: var(--accent-ink);
}

.modal__actions .button--primary.button--danger:hover {
  background: var(--danger-hover);
}

/* ---------- Modal forms ---------- */

/* A form that brings its own body and footer takes the padding over from the
   card, so the heading and the buttons stay put while the fields scroll. */
.modal__card:has(.form--modal) {
  display: flex;
  flex-direction: column;
  max-height: min(86dvh, 760px);
  padding: 0;
}

.modal__card:has(.form--modal) .modal__header,
.page-card:has(.form--modal) .modal__header {
  margin: 0;
  padding: 20px 24px 16px;
  border-bottom: 1px solid var(--line);
}

.form--modal {
  flex: 1;
  min-height: 0;
  gap: 0;
}

.form__body {
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-height: 0;
  padding: 20px 24px;
  overflow-y: auto;
  /* Reaching the end of the fields does not start scrolling the page behind. */
  overscroll-behavior: contain;
}

/* A line about the whole form rather than one field. */
.form__note {
  margin: 0;
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* Date and account are short answers and belong on one line; the fields above
   them are the ones worth a full row each. */
.form__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.form__footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 16px 24px;
  border-top: 1px solid var(--line);
  background: var(--surface);
}

.field__hint {
  color: var(--ink-muted);
  font-size: 12px;
}

/* The currency sits in the field rather than in the label: the amount reads as
   money while it is being typed. */
.field__prefix {
  position: absolute;
  top: 50%;
  left: 12px;
  transform: translateY(-50%);
  color: var(--ink-muted);
  pointer-events: none;
}

.field__input--with-prefix {
  padding-left: 42px;
}

/* The unit after the number: "100 %". */
.field__suffix {
  position: absolute;
  top: 50%;
  right: 12px;
  transform: translateY(-50%);
  color: var(--ink-muted);
  pointer-events: none;
}

.field__input--with-suffix {
  padding-right: 32px;
}

.field__input--amount {
  font-size: 20px;
  font-weight: 600;
}

/* ---------- Segmented choice ---------- */

.segmented-field {
  border: 0;
  padding: 0;
  margin: 0;
}

/* Two answers, both visible, one click apart: cheaper than opening a select to
   read what the other option even was. */
.segmented {
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 4px;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--canvas);
}

.segmented__input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

/* The labels sit above the pill (below), and only their words change colour. */
.segmented__label {
  position: relative;
  z-index: 1;
  padding: 8px 12px;
  border-radius: 7px;
  color: var(--ink-muted);
  font-size: 14px;
  text-align: center;
  cursor: pointer;
  user-select: none;
  transition: color var(--duration-fast) ease, scale var(--duration-press) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .segmented__label:hover {
    color: var(--ink);
  }
}

.segmented__input:checked + .segmented__label {
  color: var(--ink);
  font-weight: 600;
}

.segmented__input:focus-visible + .segmented__label {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* One pill for the whole control, which travels to the answer picked: the
   choice reads as moving from one side to the other, not as one light going
   out and another coming on. Two answers, so half the width less the gaps. */
.segmented::before {
  content: "";
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 4px;
  width: calc((100% - 12px) / 2);
  border-radius: 7px;
  background: var(--surface);
  box-shadow: var(--shadow-card);
  transition: translate 220ms var(--ease-in-out), background-color var(--duration-fast) ease;
}

.segmented:has(.segmented__input:nth-of-type(2):checked)::before {
  translate: calc(100% + 4px) 0;
}

.segmented:not(:has(.segmented__input:checked))::before {
  opacity: 0;
}

/* Three answers (income, expense, transfer): a third of the width each. */
.segmented--three::before {
  width: calc((100% - 16px) / 3);
}

.segmented--three:has(.segmented__input:nth-of-type(3):checked)::before {
  translate: calc(200% + 8px) 0;
}

/* Direction keeps the two colours the summary and the ledger already use. A
   transfer is neither, so it wears the brand's quieter blue. */
.segmented:has(.segmented__input:checked + .segmented__label--transfer)::before {
  background: var(--brand-surface);
}

.segmented__input:checked + .segmented__label--transfer {
  color: var(--brand);
}
/* Direction keeps the two colours the summary and the ledger already use. */
.segmented:has(.segmented__input:checked + .segmented__label--income)::before {
  background: var(--success-surface);
}

.segmented__input:checked + .segmented__label--income {
  color: var(--success);
}

.segmented:has(.segmented__input:checked + .segmented__label--expense)::before {
  background: var(--danger-surface);
}

.segmented__input:checked + .segmented__label--expense {
  color: var(--danger);
}

/* Status wears the same two colours as its badge in the ledger, so the choice
   made here is recognisable in the row it produces. */
.segmented:has(.segmented__input:checked + .segmented__label--paid)::before {
  background: var(--success-surface);
}

.segmented__input:checked + .segmented__label--paid {
  color: var(--success);
}

.segmented:has(.segmented__input:checked + .segmented__label--pending)::before {
  background: var(--warning-surface);
}

.segmented__input:checked + .segmented__label--pending {
  color: var(--warning);
}

@media (prefers-reduced-motion: no-preference) {
  .segmented__label:active {
    scale: 0.97;
  }
}

/* Reduced motion: the pill changes colour where it lands, without travelling. */
@media (prefers-reduced-motion: reduce) {
  .segmented::before {
    transition: background-color var(--duration-fast) ease;
  }
}

/* A phone has no room for a floating card: the form comes up from the bottom
   edge and keeps its footer above the home indicator. */
@media (max-width: 560px) {
  .modal--form {
    max-width: 100%;
    margin: auto auto 0;
    border-width: 1px 0 0;
    border-radius: var(--radius-card) var(--radius-card) 0 0;
  }

  .modal__card:has(.form--modal) {
    max-height: 92dvh;
  }

  .form__row {
    grid-template-columns: 1fr;
  }

  .form__footer {
    padding-bottom: max(16px, env(safe-area-inset-bottom));
  }

  .form__footer .button {
    flex: 1;
  }
}

/* A sheet rests on the bottom edge, so it comes up from there and goes back
   down there, rather than rising a few pixels out of thin air. */
@media (max-width: 560px) and (prefers-reduced-motion: no-preference) {
  .modal--form {
    translate: 0 100%;
    transition: opacity 200ms var(--ease-drawer), translate 200ms var(--ease-drawer),
      overlay 200ms allow-discrete, display 200ms allow-discrete;
  }

  .modal--form[open] {
    translate: 0 0;
    transition-duration: 320ms;
  }

  @starting-style {
    .modal--form[open] {
      translate: 0 100%;
    }
  }
}

/* A form sent back refused comes in a new dialog that takes the place of the
   one on screen (see shared/_modal_or_page): it starts where it will end, so
   nothing fades or slides in again. Last, to win over every entrance above. */
@starting-style {
  .modal--settled[open],
  .modal--settled[open]::backdrop {
    opacity: 1;
    translate: 0 0;
  }
}

/* ---------- Form pages ---------- */

/* A modal's form reached by its own URL, or without JavaScript: the same card
   at the same width, rather than fields stretched across the window. */
.page-back {
  margin: 0 0 var(--space-4);
}

.page-back a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--ink-muted);
  font-size: var(--text-md);
  text-decoration: none;
}

@media (hover: hover) and (pointer: fine) {
  .page-back a:hover {
    color: var(--ink);
  }
}

.page-card {
  max-width: 520px;
  padding: 24px;
  /* The footer's band stays inside the rounded corners. */
  overflow: hidden;
}

.page-card:has(.form--modal) {
  padding: 0;
}

/* ---------- Flash toasts ---------- */

/* Top centre. It floats over the nav, whose middle is empty: the links sit
   left and the account controls sit right. */
.toasts {
  position: fixed;
  top: calc(16px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(360px, calc(100vw - 32px));
}

/* In from the top and out the same way, so its path says where it lives. A
   transition rather than keyframes: one arriving while another leaves picks
   up from wherever it is instead of restarting. */
.toast {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px 14px;
  border: 1px solid transparent;
  border-radius: 8px;
  box-shadow: var(--shadow-float);
  font-size: 14px;
  transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
}

@starting-style {
  .toast {
    opacity: 0;
    transform: translateY(-12px);
  }
}

.toast.is-leaving {
  opacity: 0;
  transform: translateY(-12px);
  transition-duration: 150ms;
}

.toast__message {
  flex: 1;
}

.toast__close {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font-size: 18px;
  line-height: 1.2;
  opacity: 0.6;
  cursor: pointer;
}

.toast__close:hover {
  opacity: 1;
}

.toast--alert {
  border-color: var(--danger);
  background: var(--danger-surface);
  color: var(--danger);
}

.toast--notice {
  border-color: var(--success);
  background: var(--success-surface);
  color: var(--success);
}

/* Reduced motion keeps the fade, which says "this appeared", and drops the
   slide. The controller times the removal off whatever duration is left. */
@media (prefers-reduced-motion: reduce) {
  .toast {
    transition: opacity 200ms var(--ease-out);
  }

  @starting-style {
    .toast {
      transform: none;
    }
  }

  .toast.is-leaving {
    transform: none;
  }
}

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

/* The chart stops being a side column before the ledger gets too tight to
   read. */
@media (max-width: 1080px) {
  .dash {
    grid-template-columns: minmax(0, 1fr);
  }

  .chart__dial {
    max-width: 168px;
  }
}

@media (max-width: 760px) {
  .summary__value {
    font-size: 26px;
  }

  .ledger-panel,
  .list-panel {
    --panel-pad: var(--space-4);
  }

  .chart {
    padding: var(--space-4);
  }
}

/* A phone keeps 16px from the glass: the gutter the platforms themselves use. */
@media (max-width: 560px) {
  :root {
    --page-gutter: 16px;
  }
}

/* The table never scrolls sideways: that drags the whole page with it and
   clips the open row menu. It gives up columns instead, the least needed
   first, and what they said moves into the short line under the description. */
@container ledger (max-width: 900px) {
  .ledger__account,
  .ledger__tags {
    display: none;
  }

  .entry__aside {
    display: flex;
  }

  .entry__when,
  .entry__state {
    display: none;
  }
}

@container ledger (max-width: 640px) {
  .ledger__status,
  .ledger__date {
    display: none;
  }

  .entry__when,
  .entry__state {
    display: inline;
  }

  /* Edge to edge inside the panel, like a list on a phone: the width goes to
     the description rather than to padding. */
  .ledger-panel .ledger {
    width: calc(100% + var(--panel-pad) * 2);
    margin-inline: calc(var(--panel-pad) * -1);
  }

  .ledger-panel .ledger th:first-child,
  .ledger-panel .ledger th:last-child {
    border-radius: 0;
  }

  .ledger th,
  .ledger td {
    padding: var(--space-3) var(--space-2);
  }

  .ledger .ledger__select {
    padding-left: var(--space-1);
  }

  .ledger .ledger__amount {
    padding-left: var(--space-1);
    font-size: var(--text-md);
  }

  .ledger .ledger__actions {
    padding-inline: 0 var(--space-1);
  }

  .entry {
    gap: 10px;
  }

  .entry__name {
    font-size: var(--text-md);
  }

  .entry__avatar {
    width: 28px;
    height: 28px;
  }

  .filters__selects .filters__select {
    flex: 0 0 auto;
  }
}

/* ---------- WhatsApp ---------- */

.whatsapp-lead {
  max-width: 60ch;
  margin: var(--space-2) 0 0;
  color: var(--ink-muted);
}

.whatsapp {
  display: block;
  max-width: 720px;
  margin-top: var(--space-5);
}

.whatsapp-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-6);
}

.whatsapp-panel__head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

.whatsapp-panel__error {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--warning-surface);
  color: var(--warning);
  font-size: var(--text-md);
}

.whatsapp-panel__form {
  max-width: 360px;
}

.whatsapp-panel__actions {
  display: flex;
  justify-content: flex-end;
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}

/* The state at a glance, sitting on the title's first line. */
.whatsapp-dot {
  flex: none;
  width: 10px;
  height: 10px;
  margin-top: 6px;
  border-radius: 50%;
  background: var(--line-strong);
}

.whatsapp-dot--waiting {
  background: var(--warning);
}

.whatsapp-dot--danger {
  background: var(--danger);
}

.whatsapp-check {
  display: grid;
  flex: none;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--success-surface);
  color: var(--success);
}

.whatsapp-check path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The code on the left, what to do with it on the right; stacked on a phone,
   code first, since that is what the other phone is pointed at. */
.whatsapp-pairing {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 560px) {
  .whatsapp-pairing {
    grid-template-columns: 1fr;
  }

  .whatsapp-qr {
    justify-self: center;
  }
}

/* A QR code is read off a light background whatever the theme: phone cameras
   struggle with an inverted one, so the square stays white in the dark. */
.whatsapp-qr {
  display: grid;
  place-items: center;
  width: 232px;
  aspect-ratio: 1;
  padding: var(--space-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: #ffffff;
}

.whatsapp-qr__image {
  display: block;
  width: 100%;
  height: auto;
  image-rendering: pixelated;
}

.whatsapp-qr__placeholder {
  width: 100%;
  height: 100%;
  border-radius: var(--radius-sm);
  background: #eeeeee;
}

.whatsapp-steps {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

.whatsapp-steps__title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 600;
}

.whatsapp-steps__list {
  margin: 0;
  padding-left: 1.2em;
  color: var(--ink-muted);
  font-size: var(--text-md);
  line-height: 1.6;
}

.whatsapp-code {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px dashed var(--line);
}

.whatsapp-code .card__note {
  margin: 0;
}

.whatsapp-code__value {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
}

.whatsapp-code__char {
  display: grid;
  place-items: center;
  width: 32px;
  height: 40px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--canvas);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-lg);
  font-weight: 600;
}

.whatsapp-code__dash {
  padding: 0 var(--space-1);
  color: var(--ink-muted);
}

.whatsapp-number {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-2) var(--space-5);
  margin: 0;
}

.whatsapp-number dt {
  color: var(--ink-muted);
}

.whatsapp-number dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

.whatsapp-panel__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.whatsapp-panel__actions.whatsapp-panel__buttons {
  justify-content: flex-end;
}

/* The admin's two WhatsApp pages, side by side under the title. */
.tabs {
  display: inline-flex;
  gap: var(--space-1);
  margin-top: var(--space-3);
  padding: var(--space-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--surface);
}

.tabs__link {
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  color: var(--ink-muted);
  font-size: var(--text-md);
  text-decoration: none;
}

.tabs__link:hover {
  color: var(--ink);
}

.tabs__link[aria-current="page"] {
  background: var(--accent);
  color: var(--accent-ink);
}

/* ---------- Your own WhatsApp (profile) ---------- */

.whatsapp-activation {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 560px) {
  .whatsapp-activation {
    grid-template-columns: 1fr;
  }
}

.whatsapp-activation__code {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.countdown {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.countdown__track {
  height: 4px;
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--line);
}

/* Scaled rather than resized: the bar empties on the compositor, a second at
   a time, without laying the page out again. */
.countdown__bar {
  height: 100%;
  border-radius: inherit;
  background: var(--success);
  transform-origin: left;
}

.countdown.is-ending .countdown__bar {
  background: var(--warning);
}

.countdown__label {
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.countdown__label strong {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.countdown.is-ending .countdown__label strong {
  color: var(--warning);
}

/* The message to send, drawn as it will look once sent. */
.whatsapp-preview {
  align-self: flex-start;
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius) var(--radius) 4px var(--radius);
  background: var(--success-surface);
  color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-md);
  overflow-wrap: anywhere;
}

.whatsapp-open {
  align-self: flex-start;
}

/* ---------- WhatsApp conversations (admin) ---------- */

.conversations {
  max-width: 720px;
  margin: var(--space-5) 0 0;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  list-style: none;
}

.conversations li + li {
  border-top: 1px solid var(--line);
}

.conversation {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  color: inherit;
  text-decoration: none;
}

.conversation:hover {
  background: var(--canvas);
}

.conversation__avatar {
  display: grid;
  flex: none;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--brand-surface);
  color: var(--brand);
  font-weight: 600;
}

.conversation__main {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.conversation__top,
.conversation__bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.conversation__name,
.conversation__preview {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.conversation__time,
.conversation__preview {
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.conversation__time {
  flex: none;
  font-variant-numeric: tabular-nums;
}

.conversation__you {
  color: var(--ink);
}

.conversation__user {
  flex: none;
  text-transform: none;
}

.chat {
  display: flex;
  flex-direction: column;
  max-width: 720px;
  /* Tall enough to read a conversation, short enough that the header stays
     on screen while the thread scrolls under it. */
  height: min(75vh, 760px);
  height: min(75dvh, 760px);
  overflow: hidden;
}

.chat__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--line);
}

.chat__who {
  flex: 1;
  min-width: 0;
}

.chat__name {
  margin: 0;
  font-size: var(--text-lg);
}

.chat__who .card__note {
  margin: 0;
}

.chat__thread {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-5);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--canvas);
  list-style: none;
}

.chat__day {
  align-self: center;
  margin: var(--space-3) 0 var(--space-1);
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* Theirs on the left, the app's on the right, each with the corner nearest
   the edge squared off, the way every chat app draws them. */
.bubble {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-width: min(75%, 480px);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius) var(--radius) var(--radius) 4px;
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

.bubble--outbound {
  align-self: flex-end;
  border-radius: var(--radius) var(--radius) 4px var(--radius);
  background: var(--success-surface);
}

.bubble__body {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.bubble__time {
  align-self: flex-end;
  color: var(--ink-muted);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

/* Motion here marks change and nothing else. A panel rises in only when the
   state is new (the frame morphs, so an unchanged one is left still), the dot
   breathes while the panel is waiting on the phone, the first QR code fades up
   out of its placeholder, the pairing code's boxes land one after another in
   the order they are typed, and the tick draws itself on connecting. */
@media (prefers-reduced-motion: no-preference) {
  .whatsapp-panel {
    animation: whatsapp-rise 320ms var(--ease-out) both;
  }

  .whatsapp-dot--waiting {
    animation: whatsapp-breathe 1.6s ease-in-out infinite;
  }

  .whatsapp-qr__placeholder {
    animation: whatsapp-shimmer 1.2s ease-in-out infinite alternate;
  }

  .whatsapp-qr__image {
    animation: whatsapp-appear 280ms var(--ease-out) both;
  }

  .whatsapp-code__char {
    animation: whatsapp-land 260ms var(--ease-out) both;
    animation-delay: calc(var(--i) * 45ms + 80ms);
  }

  .whatsapp-check {
    animation: whatsapp-pop 360ms var(--ease-out) both;
  }

  /* The bar empties a second at a time; the transition joins the steps. */
  .countdown__bar {
    transition: transform 1s linear, background-color 400ms ease;
  }

  /* A message that arrives while the thread is open comes up from its own
     side; the ones there when it opened do not move. */
  .bubble.is-new {
    animation: whatsapp-bubble 300ms var(--ease-out) both;
    transform-origin: bottom left;
  }

  .bubble--outbound.is-new {
    transform-origin: bottom right;
  }

  .tabs__link {
    transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
  }

  .whatsapp-check path {
    stroke-dasharray: 22;
    stroke-dashoffset: 22;
    animation: whatsapp-draw 360ms var(--ease-out) 180ms forwards;
  }
}

@keyframes whatsapp-rise {
  from { opacity: 0; translate: 0 8px; }
}

@keyframes whatsapp-breathe {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--warning) 45%, transparent); }
  60% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--warning) 0%, transparent); }
}

@keyframes whatsapp-shimmer {
  to { opacity: 0.5; }
}

@keyframes whatsapp-appear {
  from { opacity: 0; scale: 0.96; }
}

@keyframes whatsapp-land {
  from { opacity: 0; translate: 0 6px; }
}

@keyframes whatsapp-pop {
  from { scale: 0.6; opacity: 0; }
}

@keyframes whatsapp-draw {
  to { stroke-dashoffset: 0; }
}

@keyframes whatsapp-bubble {
  from { opacity: 0; scale: 0.92; translate: 0 8px; }
}

/* ---------- Reports ---------- */

/* Money in is green and money out red, as everywhere else in the app. The
   obvious pair is hard to tell apart for red-green colour blindness, so these
   steps were picked with the validator: a darker green against a deeper red
   (just inside the floor on white, comfortably clear on the dark page). The
   floor is only legal with a second channel, and there is one everywhere
   they are used: a point is round for income and square for expenses, each
   line names itself, and the table says every number. */
:root {
  --series-income: light-dark(#2a9d5c, #36aa69);
  --series-expense: light-dark(#c93a2e, #cf3b2a);
  --series-committed: #7c56d6;
  --series-heat: #2f7ae5;
}

/* The span switch: four links with a lifted pill under the chosen one, the
   theme switch's look. The pill is placed by which option has aria-current,
   which the span-switch controller moves on the click, so it slides at once. */
.span-switch {
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--canvas);
  --active: 0;
}

.span-switch:has(> :nth-child(2)[aria-current]) { --active: 1; }
.span-switch:has(> :nth-child(3)[aria-current]) { --active: 2; }
.span-switch:has(> :nth-child(4)[aria-current]) { --active: 3; }

.span-switch::before {
  content: "";
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 2px;
  width: calc((100% - 10px) / 4);
  border-radius: var(--radius-pill);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  translate: calc(var(--active) * (100% + 2px)) 0;
  transition: translate 240ms var(--ease-in-out);
}

.span-switch__option {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  min-height: 32px;
  padding: 5px 14px;
  border-radius: var(--radius-pill);
  color: var(--ink-muted);
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--duration-fast) ease, scale var(--duration-press) var(--ease-out);
}

.span-switch__option[aria-current] {
  color: var(--ink);
  font-weight: 600;
}

.span-switch__option:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

@media (hover: hover) and (pointer: fine) {
  .span-switch__option:hover {
    color: var(--ink);
  }
}

@media (pointer: coarse) {
  .span-switch__option {
    min-height: var(--target-touch);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .span-switch__option:active {
    scale: 0.97;
  }
}

@media (prefers-reduced-motion: reduce) {
  .span-switch::before {
    transition: none;
  }
}

@media (max-width: 560px) {
  .span-switch {
    width: 100%;
  }

  .span-switch__option {
    padding-inline: 6px;
  }
}

.report-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-4);
}

.report-meta__range {
  margin: 0;
  color: var(--ink-muted);
  font-size: var(--text-md);
}

.report-meta__range::first-letter {
  text-transform: uppercase;
}

.report-summary {
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  margin-top: var(--space-4);
}

/* On a phone the four averages are equals, two by two: the dashboard's first
   card spans the row because the balance is the headline, and here nothing is. */
@media (max-width: 560px) {
  .report-summary .summary__card:first-child {
    grid-column: auto;
  }

  .report-summary .summary__card:first-child .summary__value {
    font-size: 20px;
  }
}

.report-link {
  color: var(--ink);
  text-underline-offset: 3px;
}

.report-card {
  margin-top: var(--space-4);
  padding: var(--space-5);
}

.report-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3) var(--space-5);
  flex-wrap: wrap;
}

.report-card__foot {
  margin: var(--space-3) 0 0;
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* Commitments and tags side by side when there is room for both. */
.report-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
  align-items: start;
  gap: var(--space-4);
  margin-top: var(--space-4);
}

.report-pair > .report-card {
  margin-top: 0;
}

/* The legend: always there for two series, so which bar is which never rests
   on telling blue from red. */
.series-key {
  display: flex;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.series-key li {
  display: flex;
  align-items: center;
  gap: 6px;
}

.series-key__mark--income { --swatch: var(--series-income); }
.series-key__mark--expense { --swatch: var(--series-expense); }

/* A short stretch of line with its point on it, the shape included. */
.series-key__mark {
  position: relative;
  width: 18px;
  height: 2px;
  border-radius: 1px;
  background: var(--swatch);
}

.series-key__mark::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--swatch);
  translate: -50% -50%;
}

.series-key__mark--expense::after {
  width: 7px;
  height: 7px;
  border-radius: 1.5px;
}

/* ---------- Report focus panel ---------- */

/* What a chart is saying. Every panel is there, stacked in one cell, and only
   one shows: the span's totals while nothing is pointed at, then the month in
   focus (the chart controller puts is-active on it). A change of focus is a
   crossfade in place, so the chart under it never moves. */
.focus {
  display: grid;
  margin-top: var(--space-4);
}

.focus__panel {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  translate: 0 3px;
  transition: opacity 150ms ease, translate 150ms var(--ease-out), visibility 0s linear 150ms;
}

.report-card:not([data-active]) .focus__panel--default,
.focus__panel.is-active {
  opacity: 1;
  visibility: visible;
  translate: 0 0;
  transition-delay: 0s;
}

.focus__title {
  margin: 0;
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.focus__title::first-letter {
  text-transform: uppercase;
}

.focus__figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-6);
  margin: var(--space-1) 0 0;
}

.focus__figures dt {
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

.focus__figures dd {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
}

.focus__figures small {
  margin-left: 4px;
  color: var(--ink-muted);
  font-size: var(--text-xs);
  font-weight: 400;
}

.focus__headline {
  margin: 2px 0 0;
  font-size: var(--text-xl);
  font-weight: 600;
  letter-spacing: -0.02em;
}

.focus__hint {
  margin: var(--space-1) 0 0;
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

.focus__list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-sm);
}

.focus__list li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
}

.focus__list strong {
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .focus__panel {
    translate: 0 0;
  }
}

/* The panel keeps the height of its tallest month, so the chart under it
   never jumps; on a phone the figures shrink so that height stays small. */
@media (max-width: 560px) {
  .focus__figures {
    gap: 2px var(--space-4);
  }

  .focus__figures dd {
    font-size: var(--text-base);
  }

  .focus__figures small {
    display: block;
    margin-left: 0;
  }
}

/* ---------- Report bars ---------- */

/* Columns drawn in HTML rather than SVG, so the labels stay crisp at any width
   and each month is a real button: the whole column, not just its bars, is
   what a pointer or a finger hits. The gridlines sit behind, in the same box
   less the band the month names take at the bottom. */
.bars {
  --axis: 64px;
  --label-band: 26px;
  position: relative;
  margin-top: var(--space-4);
  padding-left: var(--axis);
}

.bars__grid {
  position: absolute;
  inset: 0 0 var(--label-band) 0;
  pointer-events: none;
}

/* The baseline: the one rule that is a touch stronger than the grid. */
.bars__grid::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: var(--axis);
  border-top: 1px solid var(--line-strong);
}

/* A tick is a line with its figure to its left, in the axis column. */
.bars__tick {
  position: absolute;
  right: 0;
  bottom: var(--at);
  left: 0;
  display: grid;
  grid-template-columns: calc(var(--axis) - 8px) minmax(0, 1fr);
  justify-items: end;
  align-items: center;
  column-gap: 8px;
  height: 0;
  color: var(--ink-muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.bars__tick::after {
  content: "";
  justify-self: stretch;
  border-top: 1px solid var(--line);
}

.bars__plot {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--count), minmax(0, 1fr));
  height: 240px;
}

.bars__column {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: background-color var(--duration-fast) ease;
}

.bars__column:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.bars__column.is-active {
  background: color-mix(in srgb, var(--canvas) 70%, transparent);
}

.bars__pair {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 2px;
  height: calc(100% - var(--label-band));
}

/* Thin, capped, rounded where the data ends and square on the baseline. */
.bars__bar {
  width: clamp(5px, 32%, 22px);
  height: var(--h);
  border-radius: 4px 4px 0 0;
  transform-origin: 50% 100%;
  transition: opacity 200ms ease;
}

.bars--single .bars__bar {
  width: clamp(8px, 55%, 28px);
}

.bars__bar--committed { background: var(--series-committed); }

.report-card[data-active] .bars__column:not(.is-active) .bars__bar {
  opacity: 0.3;
}

.bars__label {
  display: grid;
  place-items: center;
  height: var(--label-band);
  overflow: hidden;
  color: var(--ink-muted);
  font-size: var(--text-xs);
  white-space: nowrap;
}

.bars__column.is-active .bars__label {
  color: var(--ink);
  font-weight: 600;
}

/* The bars rise from the baseline when the report arrives, one month after
   the other, so the eye reads the span left to right the way time went. */
@media (prefers-reduced-motion: no-preference) {
  .bars__bar {
    animation: bar-rise 520ms var(--ease-out) both;
    animation-delay: calc(var(--i, 0) * 35ms);
  }
}

@keyframes bar-rise {
  from { scale: 1 0; }
}

@media (max-width: 560px) {
  .bars {
    --axis: 52px;
  }

  .bars__tick {
    font-size: 10px;
  }

  .bars__plot {
    height: 200px;
  }
}

/* ---------- Report lines ---------- */

/* Room above the plot for a line's name when its last point is at the top. */
.bars.lines {
  margin-top: var(--space-8);
}

/* Laid over the plot, clear of the axis and the month names, and out of the
   pointer's way: the columns underneath take the clicks. The lines are drawn
   in a box that stretches with the card, their stroke kept at 2px whatever
   the stretch; the points are HTML, so they stay round. */
.lines__layer {
  position: absolute;
  top: 0;
  right: 0;
  bottom: var(--label-band);
  left: var(--axis);
  z-index: 1;
  pointer-events: none;
}

.lines__svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.lines__line {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.lines__line--income { stroke: var(--series-income); }
.lines__line--expense { stroke: var(--series-expense); }

/* The month still being spent: its figures are not final. */
.lines__line.is-partial {
  opacity: 0.4;
}

/* What was left over (or missing): a wash between the two lines. */
.lines__area {
  fill: var(--ink);
  opacity: 0.045;
}

.lines__dot {
  --swatch: var(--series-income);
  position: absolute;
  left: var(--x);
  bottom: var(--y);
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--swatch);
  box-shadow: 0 0 0 2px var(--surface);
  translate: -50% 50%;
  transition: scale 180ms var(--ease-out), opacity 180ms ease;
}

.lines__dot--expense {
  --swatch: var(--series-expense);
  width: 8px;
  height: 8px;
  border-radius: 2px;
}

.lines__dot.is-partial {
  background: var(--surface);
  box-shadow: inset 0 0 0 2px var(--swatch), 0 0 0 2px var(--surface);
}

.lines__dot.is-active {
  scale: 1.5;
}

.report-card[data-active] .lines__dot:not(.is-active) {
  opacity: 0.35;
}

/* A line's name, to the left of its last settled point: above it for the
   higher line, below for the lower. Text in text colours; the line beside it
   carries the colour. */
.lines__end {
  position: absolute;
  left: var(--x);
  bottom: var(--y);
  padding: 1px 4px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  color: var(--ink-muted);
  font-size: var(--text-xs);
  white-space: nowrap;
  translate: calc(-100% - 6px) -6px;
  transition: opacity 180ms ease;
}

.lines__end--below {
  translate: calc(-100% - 6px) calc(100% + 6px);
}

.report-card[data-active] .lines__end {
  opacity: 0;
}

/* The lines draw themselves left to right when the report arrives, the way
   time went, and each point lands as the line reaches it. Filled backwards
   only: once done, the animations let go, so focus can still grow a point
   and fade the others. */
@media (prefers-reduced-motion: no-preference) {
  .lines__svg {
    animation: line-draw 800ms var(--ease-in-out) backwards;
  }

  .lines__dot {
    animation: dot-land 280ms var(--ease-out) backwards;
    animation-delay: calc(var(--i) / var(--n) * 800ms + 60ms);
  }

  .lines__end {
    animation: dot-land 240ms var(--ease-out) 820ms backwards;
  }
}

/* Negative insets, so the stroke along the baseline and the edges is never
   cut while it draws. */
@keyframes line-draw {
  from { clip-path: inset(-12px 100% -12px -12px); }
  to { clip-path: inset(-12px -12px -12px -12px); }
}

@keyframes dot-land {
  from { opacity: 0; scale: 0.4; }
}

/* The chart's twin in figures. */
.report-table {
  margin-top: var(--space-4);
}

.report-table summary {
  width: fit-content;
  color: var(--ink-muted);
  font-size: var(--text-sm);
  cursor: pointer;
}

.report-table summary:hover {
  color: var(--ink);
}

.report-table__scroll {
  overflow-x: auto;
}

.report-table table {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
}

.report-table th[scope="row"] {
  font-weight: 400;
}

.report-table th[scope="row"]::first-letter {
  text-transform: uppercase;
}

.is-number {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

th.is-number {
  text-align: right;
}

/* ---------- Categories over time ---------- */

/* One hue, light to strong, each row against its own busiest month: the table
   shows the shape of each category's spending, not which is biggest (the
   total column says that). The gaps between cells are the surface showing
   through, so neighbouring tiles read apart without borders. */
.heat {
  margin-top: var(--space-4);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}

.heat__table {
  width: 100%;
  min-width: max-content;
  margin: 0;
  border: 0;
  border-collapse: separate;
  border-spacing: 2px;
  font-size: var(--text-sm);
}

.heat__table th,
.heat__table td {
  padding: 7px 10px;
  border-bottom: 0;
}

.heat__table thead th {
  font-size: var(--text-xs);
}

.heat__table abbr {
  text-decoration: none;
}

.heat__table thead th.is-compared {
  color: var(--ink);
  font-weight: 600;
}

/* The names stay put while the months scroll under them on a phone. */
.heat__name {
  position: sticky;
  left: 0;
  z-index: 1;
  min-width: 150px;
  background: var(--surface);
  text-align: left;
  font-weight: 400;
}

.heat__cell {
  border-radius: 6px;
  background: color-mix(in srgb, var(--series-heat) calc(var(--level) * 34%), transparent);
  color: var(--ink);
}

.heat__cell.is-compared {
  box-shadow: inset 0 0 0 1px var(--line-strong);
}

.heat__total {
  font-weight: 600;
}

.heat__trend {
  min-width: 96px;
}

.heat__toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin: -4px -6px;
  padding: 4px 6px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.heat__toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 0;
}

@media (hover: hover) and (pointer: fine) {
  .heat__toggle:hover {
    background: var(--canvas);
  }
}

.heat__chevron {
  color: var(--ink-muted);
  transition: rotate 200ms var(--ease-out);
}

.heat__toggle[aria-expanded="true"] .heat__chevron {
  rotate: 90deg;
}

/* A row without a chevron still starts where the names beside chevrons do. */
.heat__plain {
  display: inline-block;
  padding-left: calc(1.05em + 4px);
}

.heat__row--child .heat__name {
  padding-left: 30px;
  color: var(--ink-muted);
}

.heat__row--child .heat__total {
  font-weight: 400;
}

@media (prefers-reduced-motion: no-preference) {
  .heat__children:not([hidden]) tr {
    animation: heat-open 220ms var(--ease-out) both;
  }
}

@keyframes heat-open {
  from { opacity: 0; translate: 0 -4px; }
}

/* ---------- Tags in the report ---------- */

.tag-report {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

.tag-report__row {
  display: grid;
  grid-template-columns: minmax(80px, 140px) minmax(0, 1fr);
  align-items: center;
  gap: var(--space-3);
}

.tag-report__row .tag {
  justify-self: start;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tag-report__bars {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.tag-report__line {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
}

.tag-report__track {
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--canvas);
  overflow: hidden;
}

.tag-report__fill {
  display: block;
  width: var(--w);
  height: 100%;
  border-radius: var(--radius-pill);
  transform-origin: 0 50%;
}

.tag-report__fill--expense { background: var(--series-expense); }
.tag-report__fill--income { background: var(--series-income); }

.tag-report__amount {
  min-width: 96px;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

@media (prefers-reduced-motion: no-preference) {
  .tag-report__fill {
    animation: tag-fill 520ms var(--ease-out) both;
    animation-delay: calc(var(--i, 0) * 45ms + 120ms);
  }
}

@keyframes tag-fill {
  from { scale: 0 1; }
}

/* ---------- Card invoices ---------- */

/* Where an invoice stands, coloured by how much it asks of the reader: an
   open one asks nothing yet, a closed one is waiting, an overdue one is late. */
.badge--invoice-open {
  background: var(--canvas);
  color: var(--ink-muted);
}

.badge--invoice-closed {
  background: var(--warning-surface);
  color: var(--warning);
}

.badge--invoice-overdue {
  background: var(--danger-surface);
  color: var(--danger);
}

.badge--invoice-paid {
  background: var(--success-surface);
  color: var(--success);
}

.invoices__month::first-letter {
  text-transform: uppercase;
}

.invoices__row--overdue .invoices__month {
  color: var(--danger);
  font-weight: 600;
}

.invoices td form.button_to {
  display: inline;
}

.invoices__paid-label {
  display: none;
}

@container records (max-width: 600px) {
  .invoices__paid-label {
    display: inline;
    margin-right: 4px;
  }
}

/* The one action on a row that is the point of the row. */
.row-action--primary {
  border-color: var(--line);
  color: var(--ink);
}

.field__static {
  margin: 0;
  padding: 10px 0;
  font-weight: 500;
}

/* The card tile's bill. Overdue says so in red, name and amount alike. */
.card-invoice strong {
  white-space: nowrap;
}

.card-invoice--overdue,
.card-invoice--overdue strong {
  color: var(--danger);
}

.card-invoice__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
}

.card-invoice__all {
  margin-left: auto;
}

/* The payment modal's opening lines: the bill in four numbers. */
.invoice-summary {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-2);
  padding: var(--space-4);
  border-radius: var(--radius);
  background: var(--canvas);
}

.invoice-summary dt {
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

.invoice-summary dd {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: 2px 0 0;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.invoice-summary__owed {
  font-size: var(--text-lg);
}

/* ---------- Bill alerts ---------- */

/* A bill that needs seeing to, above the month's numbers. Amber while it is
   only due soon, red once it is late. It slides in, once, when the page
   arrives: it is news, not furniture. */
.bill-alerts {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: var(--space-5) 0 0;
  padding: 0;
  list-style: none;
}

.bill-alert {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid color-mix(in srgb, var(--warning) 30%, transparent);
  border-radius: var(--radius);
  background: var(--warning-surface);
  color: var(--ink);
  font-size: var(--text-md);
}

.bill-alert--overdue {
  border-color: color-mix(in srgb, var(--danger) 30%, transparent);
  background: var(--danger-surface);
}

.bill-alert__icon {
  width: 18px;
  height: 18px;
  color: var(--warning);
}

.bill-alert--overdue .bill-alert__icon {
  color: var(--danger);
}

.bill-alert__text {
  flex: 1;
  margin: 0;
}

.bill-alert__action {
  flex: none;
}

@media (prefers-reduced-motion: no-preference) {
  .bill-alert {
    animation: bill-arrive 320ms var(--ease-out) both;
  }
}

@keyframes bill-arrive {
  from { opacity: 0; translate: 0 -6px; }
}

@media (max-width: 560px) {
  .bill-alert {
    flex-wrap: wrap;
  }

  .bill-alert__text {
    flex-basis: calc(100% - 30px);
  }

  .bill-alert__action {
    margin-left: auto;
  }
}

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

/* A yes-or-no that needs a sentence to explain it (autopay, repeat every
   month): the box, a bold name and the sentence, all one target. */
.check-field {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color var(--duration-fast) ease, background-color var(--duration-fast) ease;
}

.check-field:has(.check-field__input:checked) {
  border-color: var(--brand);
  background: var(--brand-surface);
}

.check-field__input {
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  flex: none;
  accent-color: var(--brand);
}

.check-field__label {
  display: block;
  font-size: var(--text-md);
  font-weight: 600;
}

.check-field__hint {
  display: block;
  margin-top: 2px;
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* ---------- Recurring entries ---------- */

.recurrences .records__main a {
  font-weight: 500;
}

.recurrences__meta {
  display: block;
  margin-top: 2px;
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* A paused rule stays on the list, quieter, until it is resumed. */
.recurrences__row--paused td {
  opacity: 0.6;
}

.recurrences__row--paused .records__actions {
  opacity: 1;
}

.recurrences td form.button_to {
  display: inline;
}

.recurrences .is-transfer {
  color: var(--ink-muted);
}

/* Where an entry came from (a recurrence, an import, WhatsApp): a small
   mark after its category. */
.entry__origin {
  display: inline-flex;
  margin-left: 4px;
  vertical-align: -1px;
  color: var(--ink-muted);
}

.entry__origin-icon {
  width: 12px;
  height: 12px;
}

/* ---------- Budgets ---------- */

/* The dashboard's narrow column: the chart, then the budgets under it. */
.dash__side {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

.budgets-card {
  padding: 20px;
}

.budgets-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.budgets-card__link {
  font-size: var(--text-sm);
}

.budgets {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

.budgets--compact {
  gap: var(--space-4);
}

/* The state is a colour on the bar and a sentence under it: green while it
   is fine, amber once it runs low, red once it is over. */
.budget {
  --tone: var(--success);
}

.budget--warning {
  --tone: var(--warning);
}

.budget--over {
  --tone: var(--danger);
}

.budget__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.budget__name {
  min-width: 0;
  overflow: hidden;
  font-size: var(--text-md);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.budget__name a {
  color: var(--ink);
  text-decoration: none;
}

.budget__name a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.budget__figures {
  flex: none;
  margin-left: auto;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.budget__of {
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* The pencil on the dashboard card: small beside the figures, but a full
   target, and it never pushes them around. */
.budget__edit {
  flex: none;
  min-width: 28px;
  height: 28px;
  margin: -4px -6px -4px 0;
  padding: 0;
}

.budget__head .row-actions {
  flex: none;
}

.budget--over .budget__figures strong {
  color: var(--danger);
}

/* The spent part solid, what is pending after it in the same tone, fainter. */
.budget__bar {
  position: relative;
  height: 8px;
  margin-top: 6px;
  border-radius: var(--radius-pill);
  background: var(--canvas);
  box-shadow: inset 0 0 0 1px var(--line);
}

.budget__spent,
.budget__pending {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: var(--radius-pill);
  background: var(--tone);
}

.budget__spent {
  left: 0;
  width: var(--spent);
  transform-origin: 0 50%;
}

.budget__pending {
  left: var(--spent);
  width: var(--pending);
  opacity: 0.35;
}

/* Over budget: the budget's part of what was spent, fainter, then the
   excess, striped, with the budget itself as a mark between them. The mark
   rises past the bar like the pace tick does, so the two read alike. */
.budget__within,
.budget__excess {
  position: absolute;
  top: 0;
  bottom: 0;
}

.budget__within {
  left: 0;
  width: var(--limit);
  border-radius: var(--radius-pill) 0 0 var(--radius-pill);
  background: color-mix(in srgb, var(--danger) 28%, transparent);
}

.budget__excess {
  left: var(--limit);
  right: 0;
  border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
  background: repeating-linear-gradient(135deg, var(--danger) 0 6px, color-mix(in srgb, var(--danger) 50%, var(--surface)) 6px 9px);
  transform-origin: 0 50%;
}

.budget__limit {
  position: absolute;
  top: -4px;
  bottom: -4px;
  left: var(--limit);
  width: 2px;
  border-radius: 1px;
  background: var(--ink);
  translate: -1px 0;
}

@media (prefers-reduced-motion: no-preference) {
  .budget__excess {
    animation: budget-fill 600ms var(--ease-out) backwards;
    animation-delay: calc(var(--i, 0) * 60ms + 200ms);
  }
}

/* Where spending would be if it kept pace with the month. */
.budget__pace {
  position: absolute;
  top: -3px;
  bottom: -3px;
  left: var(--pace);
  width: 2px;
  border-radius: 1px;
  background: var(--ink-muted);
  translate: -1px 0;
}

.budget__note {
  margin: 5px 0 0;
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

.budget--over .budget__note {
  color: var(--danger);
}

.budget__ahead {
  color: var(--warning);
}

/* The bars fill from the left when the page arrives, one after the other,
   and what is pending fades in once the spent part is there. */
@media (prefers-reduced-motion: no-preference) {
  .budget__spent {
    animation: budget-fill 600ms var(--ease-out) backwards;
    animation-delay: calc(var(--i, 0) * 60ms + 80ms);
  }

  .budget__pending {
    animation: budget-fade 300ms ease backwards;
    animation-delay: calc(var(--i, 0) * 60ms + 560ms);
  }
}

@keyframes budget-fill {
  from { scale: 0 1; }
}

@keyframes budget-fade {
  from { opacity: 0; }
}

.form__footer-start {
  margin-right: auto;
}

/* ---------- WhatsApp alerts ---------- */

.alerts-form,
.alerts-preview {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
}

.alerts-form__title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 600;
}

.alerts-form__options {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.alerts-form__row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-4);
}

.alerts-form__row .field {
  min-width: 140px;
}

/* The message as it would land on the phone: the app's own bubble, kept to
   a phone's width, its line breaks kept. */
.alerts-preview__bubble {
  max-width: 420px;
  margin: var(--space-3) 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-radius: 14px 14px 14px 4px;
  background: light-dark(#e7fbe0, #1f3a2a);
  color: var(--ink);
  font-size: var(--text-md);
  line-height: 1.45;
  white-space: pre-line;
  box-shadow: var(--shadow-card);
}

@media (prefers-reduced-motion: no-preference) {
  .alerts-preview__bubble {
    animation: whatsapp-bubble 320ms var(--ease-out) backwards;
  }
}

/* ---------- Imports ---------- */

.page-head__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* What was imported lately, above the month: the brand's blue, since it is
   news rather than a warning. It slides in once, like the bill alerts. */
.import-notices {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: var(--space-5) 0 0;
  padding: 0;
  list-style: none;
}

.bill-alerts + .import-notices {
  margin-top: var(--space-2);
}

.import-notice {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) calc(var(--space-4) + 28px) var(--space-3) var(--space-4);
  border: 1px solid color-mix(in srgb, var(--brand) 25%, transparent);
  border-radius: var(--radius);
  background: var(--brand-surface);
}

.import-notice__icon {
  width: 18px;
  height: 18px;
  color: var(--brand);
}

.import-notice__text {
  flex: 1;
  min-width: 0;
}

.import-notice__title {
  margin: 0;
  font-size: var(--text-md);
  font-weight: 600;
}

.import-notice__detail {
  margin: 2px 0 0;
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.import-notice__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.import-notice__actions form.button_to {
  display: inline;
}

.import-notice form.button_to:has(.import-notice__close) {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
}

.import-notice__close {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
}

.import-notice__close:hover {
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  color: var(--ink);
}

@media (prefers-reduced-motion: no-preference) {
  .import-notice {
    animation: bill-arrive 320ms var(--ease-out) backwards;
  }
}

@media (max-width: 720px) {
  .import-notice {
    flex-wrap: wrap;
  }

  .import-notice__text {
    flex-basis: calc(100% - 60px);
  }
}

/* One import being looked at in the ledger: a chip above the selects. */
.filter-chip {
  grid-column: 1 / -1;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  justify-self: start;
  max-width: 100%;
  padding: 4px 4px 4px 12px;
  border-radius: var(--radius-pill);
  background: var(--brand-surface);
  color: var(--brand);
  font-size: var(--text-sm);
}

.filter-chip span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.filter-chip__icon {
  width: 14px;
  height: 14px;
}

.filter-chip__remove {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  flex: none;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
}

.filter-chip__remove:hover {
  background: color-mix(in srgb, var(--brand) 15%, transparent);
}

.filter-chip__remove .icon {
  width: 14px;
  height: 14px;
}

/* The list of imports. */
.imports__file {
  display: block;
  font-weight: 500;
  overflow-wrap: anywhere;
}

.imports__when {
  display: block;
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

.imports td form.button_to {
  display: inline;
}

.badge--import-imported { background: var(--success-surface); color: var(--success); }
.badge--import-undone,
.badge--import-discarded { background: var(--canvas); color: var(--ink-muted); }
.badge--import-reviewing { background: var(--warning-surface); color: var(--warning); }

/* ---------- Budgets in the report ---------- */

.report-kept {
  margin: 0;
  font-size: var(--text-sm);
}

.budget-table__limit {
  display: block;
  padding-left: calc(1.05em + 4px);
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* A month against its budget: the share, and a thin bar under it in the
   budget's state, striped past the budget as on the dashboard. */
.budget-cell {
  --tone: var(--success);
  min-width: 64px;
  text-align: right;
  vertical-align: middle;
}

.budget-cell--warning { --tone: var(--warning); }
.budget-cell--over { --tone: var(--danger); }

.budget-cell__share {
  display: block;
  font-variant-numeric: tabular-nums;
}

.budget-cell--over .budget-cell__share {
  color: var(--danger);
  font-weight: 600;
}

.budget-cell__bar {
  position: relative;
  display: block;
  height: 5px;
  margin-top: 4px;
  border-radius: var(--radius-pill);
  background: var(--canvas);
  box-shadow: inset 0 0 0 1px var(--line);
  overflow: hidden;
}

.budget-cell__bar::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--share);
  border-radius: inherit;
  background: var(--tone);
  transform-origin: 0 50%;
}

.budget-cell--over .budget-cell__bar::after {
  background: repeating-linear-gradient(135deg, var(--danger) 0 4px, color-mix(in srgb, var(--danger) 50%, var(--surface)) 4px 6px);
}

/* The month still under way: its share is not final. */
.budget-cell.is-partial {
  opacity: 0.55;
}

@media (prefers-reduced-motion: no-preference) {
  .budget-cell__bar::after {
    animation: budget-fill 600ms var(--ease-out) backwards;
  }
}

.budget-table__suggestion td {
  padding-top: 0;
  color: var(--ink-muted);
  font-size: var(--text-xs);
  text-align: left;
}

.budget-table__suggestion .icon {
  width: 13px;
  height: 13px;
  margin-right: 4px;
  vertical-align: -2px;
  color: var(--warning);
}
