/* Finance App styles. Tokens, resets and the generic component skins moved to
   commons-web (restructuring Phase 3): commons/css/tokens.css, base.css and
   components.css load before this file from index.html, so everything here
   wins ties against them. What remains is finance-shaped: the shell, screens,
   and the status/report/owner styling below. */

/* --- sign-in gates -------------------------------------------------------- */
.gate {
  min-height: 100vh; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: .75rem; padding: 2rem; text-align: center;
}
.gate h1 { margin: .25rem 0 0; font-size: 1.5rem; }
.gate p { margin: 0; max-width: 26rem; }

/**
 * The demo sign-in block.
 *
 * Collapsed by default and visually quieter than the Google button, because
 * this is the secondary action on a production sign-in screen — a demo that
 * competes with the real thing for attention reads as a security hole rather
 * than as an invitation.
 */
.demo { margin-top: 1.5rem; max-width: 26rem; width: 100%; text-align: left; }
.demo > summary {
  cursor: pointer; text-align: center; font-size: .9rem; opacity: .65;
  padding: .5rem; border-radius: var(--radius-sm);
}
.demo > summary:hover { opacity: 1; background: rgba(var(--ink), .07); }
.demo > p { margin: .5rem 0 .75rem; font-size: .85rem; }

.demo-persona {
  display: flex; flex-direction: column; gap: .15rem; align-items: flex-start;
  width: 100%; margin-top: .5rem; padding: .7rem .9rem; text-align: left;
  border: 1px solid rgba(var(--ink), .14); border-radius: var(--radius-sm);
  background: var(--surface);
}
.demo-persona:hover:not(:disabled) { background: var(--menu); }
.demo-name { font-weight: 600; }
.demo-note { font-size: .8rem; opacity: .65; line-height: 1.3; }

/* --- shell ---------------------------------------------------------------- */
/**
 * Phone-first. #app is a column: content scrolls, the bottom bar does not.
 * Above 900px it becomes a row and the rail replaces the bar.
 *
 * 100dvh, not 100vh: on iOS Safari 100vh is the height with the URL bar
 * *retracted*, so a bottom bar pinned to 100vh sits under the browser chrome
 * until you scroll. dvh tracks the visible viewport, which is the whole point
 * of a fixed bottom bar.
 */
#app {
  position: relative;   /* anchors #account-menu and its scrim */
  display: flex; flex-direction: column;
  height: 100dvh; overflow: hidden;
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
#content {
  flex: 1; min-width: 0; min-height: 0;
  display: flex; flex-direction: column;
  position: relative;   /* containing block for #page-overlay */
}
/* Basis 0% is written out, not left implicit as `flex: 1`. Correct here —
   #app has height: 100dvh, so this fills the space left over regardless of how
   tall the panel content is. See the .sheet-body note for what happens when the
   same shorthand meets an auto-height parent. */
#panels { flex: 1 1 0%; min-height: 0; overflow: auto; }

.topbar {
  flex: none;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: calc(.6rem + env(safe-area-inset-top)) 1rem .6rem;
  border-bottom: 1px solid rgba(var(--ink), .1);
  background: var(--navbg); backdrop-filter: blur(8px); z-index: 2;
}
.brand { display: flex; align-items: center; gap: .5rem; font-weight: 600; }
.topbar-right { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

/* --- rail (desktop) ------------------------------------------------------- */
#rail { display: none; }

.rail-head {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 12px 12px; border-bottom: 1px solid rgba(var(--ink), .08);
}
.rail-head-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.rail-head-text .household-name {
  font-size: 14px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rail-head-sub { font-size: 11.5px; opacity: .55; }

.household-avatar {
  width: 30px; height: 30px; flex: none; border-radius: var(--radius-sm);
  background: var(--accent); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 600;
}

.rail-toggle {
  flex: none; width: 28px; height: 28px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 7px; border: 1px solid rgba(var(--ink), .14);
  background: transparent; opacity: .7;
}
.rail-toggle:hover { opacity: 1; }

.rail-nav { display: flex; flex-direction: column; gap: 3px; padding: 10px 8px; }
.rail-item {
  display: flex; align-items: center; gap: 11px;
  height: 40px; padding: 0 11px;
  border: 0; border-radius: 9px; background: transparent;
  color: rgba(var(--ink), .68); font-size: 14.5px;
  text-align: left; white-space: nowrap; overflow: hidden;
}
.rail-item .icon { flex: none; display: inline-flex; opacity: .6; }
.rail-item:hover { background: rgba(var(--ink), .06); color: var(--text); }
.rail-item.active {
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  color: var(--text); font-weight: 600;
}
.rail-item.active .icon { opacity: 1; }

.rail-foot {
  margin-top: auto; padding: 8px;
  border-top: 1px solid rgba(var(--ink), .08);
}
.account-button {
  width: 100%; display: flex; align-items: center; gap: 11px;
  height: 46px; padding: 0 9px;
  border: 0; border-radius: 9px; background: transparent;
  text-align: left; white-space: nowrap; overflow: hidden;
}
.account-button:hover, .account-button[aria-expanded="true"] { background: rgba(var(--ink), .07); }
.account-button-name { font-size: 14px; }
.account-button-caret { margin-left: auto; font-size: 11px; opacity: .5; }

/* Collapsed rail. The labels are clipped to zero width rather than removed, so
   the buttons keep their accessible names — see the note in shell/nav.js. */
#rail[data-collapsed="true"] .rail-label,
#rail[data-collapsed="true"] .rail-head-text,
#rail[data-collapsed="true"] .account-button-name,
#rail[data-collapsed="true"] .account-button-caret {
  width: 0; overflow: hidden; opacity: 0;
}
#rail[data-collapsed="true"] .rail-head { flex-direction: column; }
/* Zero-width labels still leave the row's 11px padding and 11px gap on the
   left, which pushed every icon ~4px off the rail's centre line. Centring
   explicitly rather than nudging the padding, so the result does not depend on
   the label collapsing to exactly zero. */
#rail[data-collapsed="true"] .rail-item,
#rail[data-collapsed="true"] .account-button {
  justify-content: center; padding: 0; gap: 0;
}

/* --- bottom bar (phone) --------------------------------------------------- */
#bottombar {
  flex: none; display: flex;
  padding: 8px 6px calc(8px + env(safe-area-inset-bottom));
  border-top: 1px solid rgba(var(--ink), .1);
  background: var(--navbg); backdrop-filter: blur(12px);
}
.bar-item {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px;
  min-height: 48px; padding: 6px 0;
  border: 0; background: none; color: rgba(var(--ink), .6);
}
.bar-item.active { color: var(--accent); }
.bar-label { font-size: 11px; }
.bar-item.active .bar-label { font-weight: 600; }

/* --- account menu --------------------------------------------------------- */
#account-scrim { position: absolute; inset: 0; background: rgb(0 0 0 / .5); z-index: 10; }
#account-menu {
  position: absolute; z-index: 11;
  width: 264px; padding: 6px;
  border-radius: var(--radius); background: var(--menu);
  border: 1px solid rgba(var(--ink), .14);
  box-shadow: 0 18px 48px rgb(0 0 0 / .55);
  /* Phone: under the topbar avatar. Desktop rules below move it to the rail. */
  top: 60px; right: 12px;
}
.account-head { display: flex; align-items: center; gap: 10px; padding: 10px 10px 12px; }
.account-head-text { display: flex; flex-direction: column; min-width: 0; }
.account-head-text > span:first-child { font-size: 14px; font-weight: 600; }
.account-head-text > span:last-child {
  font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.menu-rule { height: 1px; background: rgba(var(--ink), .1); margin: 0 4px 6px; }
.menu-row { display: flex; align-items: center; gap: 10px; padding: 2px 10px 8px; }
.menu-row > span { font-size: 14px; opacity: .7; }
.menu-row .segmented { margin-left: auto; }
.menu-item {
  width: 100%; display: flex; align-items: center;
  min-height: 44px; padding: 0 10px;
  border: 0; border-radius: var(--radius-sm); background: none;
  font-size: 14px; text-align: left;
}
.menu-item:hover { background: rgba(var(--ink), .07); }
.danger-text { color: var(--danger); }
.menu-version { padding: 4px 10px 8px; font-size: 11.5px; opacity: .4; }

.avatar {
  width: 28px; height: 28px; flex: none; border-radius: 50%;
  background: color-mix(in srgb, var(--blue) 18%, transparent); color: var(--blue);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12.5px; font-weight: 600;
}
.avatar-lg { width: 34px; height: 34px; font-size: 14px; }
.avatar-button { padding: 4px; border: 0; background: none; border-radius: 50%; }

/* --- overlay pages -------------------------------------------------------- */
#page-overlay {
  position: absolute; inset: 0; z-index: 6;
  background: var(--bg); display: flex; flex-direction: column;
}
.page-header {
  flex: none; display: flex; align-items: center; gap: 12px;
  padding: .7rem 1rem; border-bottom: 1px solid rgba(var(--ink), .1);
}
.page-header #page-title { font-size: 17px; font-weight: 600; }
.page-header #page-meta { font-size: 12.5px; }
/* Same: #page-overlay is position: absolute; inset: 0, so the height is
   definite and a 0% basis is what is wanted. */
.page-body { flex: 1 1 0%; min-height: 0; overflow: auto; padding: 1rem; }

/* On a phone the title and its explanation cannot share a line — "Household &
   members" and "who can see and edit this household" each wrapped to two, and
   the header took a fifth of the screen before any content. Stacked into two
   rows beside the Back button instead. Grid rather than a wrapper element so
   shell/page.js can go on writing #page-title and #page-meta directly. */
@media (max-width: 899px) {
  .page-header {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    column-gap: 10px; row-gap: 1px;
    padding: .55rem .85rem;
  }
  #page-back { grid-row: 1 / span 2; min-height: var(--control-h-touch); }
  /* Truncated, not wrapped: the header is the one strip whose height the body
     below it depends on, and a two-line title pushes the content down. */
  .page-header #page-title {
    grid-area: 1 / 2;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .page-header #page-meta { grid-area: 2 / 2; line-height: 1.35; }
}

.panel { padding: 1rem; }

/* --- overflow menu -------------------------------------------------------- */
/* The comment that used to sit here said "<details> gives Escape-to-close and
   outside-click-to-close for free". It gives neither, and that one wrong
   sentence is the entire reason this menu would not close — nobody re-checked a
   claim that was written down. Dismissal now comes from lib/popover.js.

   The 34px trigger is deliberately UNCHANGED at phone size, even though it is
   under the 44px touch minimum everything else here respects. Widening it moves
   the toolbar's total width, and that row is the subject of its own fix; doing
   both at once would make it impossible to tell which change moved the number.
   See phase 3B in plans/todos/05-mobile-filters-and-forecast-plan.md. */
.overflow { position: relative; }
.overflow-trigger {
  cursor: pointer; user-select: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; line-height: 1;
  border-radius: var(--radius-sm);
  border: 1px solid rgba(var(--ink), .14);
}
.overflow-trigger:hover,
.overflow-trigger[aria-expanded="true"] { background: rgba(var(--ink), .07); }
.overflow-menu {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 4;
  min-width: 190px; padding: 6px; display: flex; flex-direction: column; gap: 2px;
  border-radius: var(--radius); background: var(--menu);
  border: 1px solid rgba(var(--ink), .14); box-shadow: 0 16px 44px rgb(0 0 0 / .45);
}
.overflow-menu button {
  border: 0; background: none; text-align: left;
  min-height: 40px; padding: 0 10px; border-radius: var(--radius-sm);
}
.overflow-menu button:hover { background: rgba(var(--ink), .07); }

/* --- stat strip ----------------------------------------------------------- */
/* The hero is 1.5fr because its number is the one the app is opened to read. */
/* The hero is 1.5fr because its number is the one the app is opened to read.
   The rest of the row is however many plain cards there are — Month has three,
   Forecast has two, and a fixed count leaves whichever screen has fewer with a
   dead column. components/stat-card.js stamps data-cards; see the note there
   for why the count is declared rather than inferred. */
.stat-strip {
  display: grid;
  grid-template-columns: 1.5fr repeat(3, 1fr);
  gap: 12px; margin-bottom: 20px;
}
.stat-strip[data-cards="1"] { grid-template-columns: 1.5fr 1fr; }
.stat-strip[data-cards="2"] { grid-template-columns: 1.5fr 1fr 1fr; }
.stat-strip[data-cards="4"] { grid-template-columns: 1.5fr repeat(4, 1fr); }
.stat-card {
  display: flex; flex-direction: column; gap: 8px;
  padding: 16px 18px; border-radius: var(--radius);
  background: var(--surface); border: 1px solid rgba(var(--ink), .1);
}
.stat-label {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em; opacity: .55;
}
.stat-hero .stat-figure { display: flex; align-items: baseline; gap: 8px; }
.stat-hero strong { font-size: 30px; font-weight: 600; letter-spacing: -.02em; }
.stat-unit { font-size: 14px; opacity: .5; }
.stat-value { font-size: 22px; font-weight: 600; letter-spacing: -.01em; }
.stat-sub { font-size: 12.5px; opacity: .55; }
.stat-bar { height: 5px; border-radius: 3px; background: rgba(var(--ink), .1); overflow: hidden; }
.stat-bar-fill { height: 100%; background: var(--accent); }

/* --- filter bar ----------------------------------------------------------- */
.filter-bar {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: 14px; flex-wrap: wrap;
}
.seg-count {
  margin-left: 6px; opacity: .55; font-variant-numeric: tabular-nums;
}
/* .month-search is now the search-box WRAPPER, not the input — the height and
   border belong to .search-input. All that is left is how it shares the filter
   bar. */
.month-search { min-width: 180px; }
.note { font-size: 12.5px; margin: 0 0 12px; }
.empty { padding: 1.5rem 0; }

/* --- month table ---------------------------------------------------------- */
.table-card {
  border: 1px solid rgba(var(--ink), .1); border-radius: var(--radius);
  overflow: auto; background: var(--surface2);
}
.month-table { border-collapse: collapse; width: 100%; font-size: 14px; }
.month-table th {
  text-align: left; padding: 10px 12px; white-space: nowrap;
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em;
  opacity: .55; font-weight: 600;
  border-bottom: 1px solid rgba(var(--ink), .1); background: var(--surface);
}
.month-table td {
  padding: 11px 12px; white-space: nowrap;
  border-bottom: 1px solid rgba(var(--ink), .07);
}
.month-table th:first-child, .month-table td:first-child { padding-left: 16px; }
.month-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.month-table tbody tr:hover { background: rgba(var(--ink), .04); }
.cell-stack { display: flex; flex-direction: column; }
.cell-title { font-weight: 500; }
.cell-sub { font-size: 12px; opacity: .45; }
.month-table .dim { opacity: .55; }
.month-table .strong { font-weight: 500; }
/* A paused item stays legible enough to find and reactivate, but must never
   read as a live cost. The action buttons keep full contrast. */
.month-table tr.is-paused td:not(.sticky-end) { opacity: .5; }

/* The actions column must stay reachable when the table scrolls sideways.
   A solid background is not decoration — without it the scrolled rows show
   through underneath. */
.month-table .sticky-end {
  position: sticky; right: 0;
  background: var(--surface2); padding-right: 16px;
}
.month-table thead .sticky-end { background: var(--surface); }

/* --- phone row list ------------------------------------------------------- */
/* .row-list is the PHONE alternative to a table and is hidden on desktop.
   .stack-list is the same row styling for lists that are a list at every
   width — the forecast's month-by-month, the balance history. Reusing
   .row-list for those hid them on desktop with the rows still in the DOM,
   which looks like a data bug and is a CSS one. */
/* EVERY base "hidden until the breakpoint" declaration lives HERE, above the
   @media block that reveals them. They have equal specificity, so the later
   rule wins — and .item-groups / .account-cards were originally declared 200
   lines further down, after the media query, which meant `display: none` won
   at every width and Items and Balances rendered nothing on a phone. Keep new
   ones in this group. */
.row-list,
.item-groups,
.account-cards,
.household-list { display: none; }

.stack-list { display: flex; flex-direction: column; }
.list-row {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--row-h-touch); padding: 11px 2px;
  border-bottom: 1px solid rgba(var(--ink), .08);
  cursor: pointer; text-align: left;
}
.list-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.dot {
  width: 8px; height: 8px; flex: none; border-radius: 50%;
  background: rgba(var(--ink), .3);
}
.dot.pos { background: var(--pos); }
.dot.warn { background: var(--warn); }
.list-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.list-title {
  font-size: 15.5px; font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.list-meta {
  font-size: 12.5px; opacity: .5;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.list-end { flex: none; display: flex; flex-direction: column; align-items: flex-end; }
.list-amount { font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.list-sub { font-size: 12px; opacity: .45; }

/* Below the breakpoint the table gives way to the list. Both are rendered;
   swapping by CSS keeps one column spec and avoids a resize listener. */
@media (max-width: 899px) {
  .table-card { display: none; }
  .row-list { display: flex; flex-direction: column; }
  .month-search { min-width: 0; }

  /* Hero full width, then the plain cards share the row beneath it. Net was
     previously stranded alone on a second row of a 2-column grid; Forecast's
     two cards were squeezed into two thirds of a 3-column one. */
  .stat-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
  .stat-strip[data-cards="1"] { grid-template-columns: minmax(0, 1fr); }
  .stat-strip[data-cards="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat-strip[data-cards="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat-hero { grid-column: 1 / -1; }
  .stat-card { padding: 12px 14px; gap: 6px; }
  .stat-value { font-size: 17px; }
  .stat-hero strong { font-size: 28px; }
  /* The budget sub-line is the first thing to go when three cards share a
     phone width — the figure above it is the point. */
  .stat-card:not(.stat-hero) .stat-sub { font-size: 11.5px; opacity: .45; }

  /* Two rows: identity and account above, the controls spanning below.
     Previously everything was one wrapping flex row, which broke the household
     name across two lines and left the month field competing with it. */
  .topbar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: .5rem .6rem;
  }
  .brand { grid-row: 1; grid-column: 1; min-width: 0; }
  .brand .household-name {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .avatar-button { grid-row: 1; grid-column: 2; }
  .topbar-right { grid-row: 2; grid-column: 1 / -1; flex-wrap: nowrap; }
  /* The month field takes the slack; "Show in" only needs its own width. */
  .topbar-right > label.inline { flex: 0 0 auto; }
  /* #month-field, NOT #month. `#month` was the old <input type="month"> and has
     matched nothing since components/month-picker.js replaced it — the picker is
     built without an id (app.js) and mounted into <div id="month-field">
     (index.html). The rule sat here looking correct while the picker shrank to
     its 127px of content in a 358px row, and the popover, at width: 100% of the
     trigger, went with it and clipped its own third column.

     The picker itself has to become a block: its base rule is `inline-block`,
     which sizes to content however wide the slot is. That rule lives ~600 lines
     BELOW this block, so this override wins on specificity (id + class), not on
     order — see web/test/css-order.mjs for why that distinction is load-bearing
     here. */
  .topbar-right #month-field { flex: 1 1 auto; min-width: 0; }
  .topbar-right #month-field .month-picker { display: block; }

  /* .panel-actions is NOT here either — its base rule is 40 lines below this
     block, so `flex-wrap: nowrap` lost the cascade and the action row went on
     wrapping to two lines on every phone. Moved down next to the base. */

  /* Items and Balances swap table for grouped list and cards, the same way
     Month swaps table for row list. */
  .item-groups, .account-cards { display: flex; }
  .household-list { display: flex; flex-direction: column; }
  /* .household-grid's single column is NOT here — see the phone block at the
     end of the household section. Only `display` bases live above this block;
     any other property overridden here loses to its own base rule further
     down the file, which is exactly how the two-column household layout
     survived onto phones. */
  /* The filter bar's own phone rules are NOT here — they override .segmented
     button, .select-toggle and .sort-control, all declared further down this
     file, so at equal specificity a rule here would lose at every width. See
     the block at the end of the sorting section. */
}

/* --- desktop -------------------------------------------------------------- */
@media (min-width: 900px) {
  #app { flex-direction: row; }
  #bottombar { display: none; }
  /* The rail carries identity on desktop; a second copy in the topbar would
     say the household name twice on one screen. */
  .topbar .brand, .topbar .avatar-button { display: none; }

  #rail {
    display: flex; flex-direction: column; flex: none;
    width: 236px; background: var(--surface2);
    border-right: 1px solid rgba(var(--ink), .12);
    padding-top: env(safe-area-inset-top);
    transition: width .18s ease;
  }
  #rail[data-collapsed="true"] { width: 64px; }

  #account-menu { top: auto; right: auto; bottom: 62px; left: 8px; }
}

/* Users who ask for less motion get the width change instantly rather than
   animated; the rail still collapses. */
@media (prefers-reduced-motion: reduce) {
  #rail { transition: none; }
}
.panel-actions { display: flex; gap: .5rem; align-items: center; margin-bottom: .75rem; flex-wrap: wrap; }
/* Wrapping these makes the row two lines high for no gain; the overflow button
   already absorbs the rarely-used actions. Below the base rule, not up in the
   shared phone block, or `flex-wrap: wrap` above wins at every width. */
@media (max-width: 899px) {
  .panel-actions { flex-wrap: nowrap; }
  .panel-actions > button { white-space: nowrap; }
}
/* --- feedback (finance statuses) ------------------------------------------ */
.badge.paid    { background: color-mix(in srgb, var(--pos)  16%, transparent); color: var(--pos); }
.badge.pending { background: color-mix(in srgb, var(--warn) 20%, transparent); color: var(--warn); }
.badge.actual  { background: color-mix(in srgb, var(--info) 16%, transparent); color: var(--info); }
.badge.stale   { background: color-mix(in srgb, var(--neg)  16%, transparent); color: var(--neg); }
/* Budget is the reference, not the reading — deliberately the quiet one. */
.badge.budget  { color: rgba(var(--ink), .62); }

/* --- generation dialog ----------------------------------------------------- */
ul.plan { margin: 0 0 .5rem; padding-left: 1.1rem; }
ul.plan .warn { color: var(--danger); font-weight: 600; }
.warnings p { margin: .4rem 0; font-size: .88rem; opacity: .8; }

/* --- summary cards & row actions ------------------------------------------ */
.cards { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: 1rem; }
.card {
  display: grid; gap: .1rem; padding: .55rem .9rem; min-width: 8rem;
  background: var(--surface);
  border: 1px solid rgba(var(--ink), .1); border-radius: 10px;
}
.card-label { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; opacity: .6; }
.card strong { font-size: 1.05rem; font-variant-numeric: tabular-nums; }

/* Two scenarios inside one card. The value column is right-aligned so the two
   figures share a decimal point and can be compared at a glance — the entire
   reason the pair sits in one card rather than two. */
.card-split { display: grid; grid-template-columns: auto 1fr; gap: .05rem .6rem; align-items: baseline; }
.card-split .card-scenario { font-size: .72rem; opacity: .6; }
.card-split strong { text-align: right; }

/* Row actions are real buttons, as in the Apps Script app — underlined link
   text gave a destructive action the same weight as a label. The base `button`
   padding is sized for the action bar and is too tall for a table row, so the
   row variant is tightened rather than the base loosened. */
.row-actions { display: flex; gap: .35rem; justify-content: flex-end; }
.row-actions button {
  white-space: nowrap; padding: .22rem .6rem; font-size: .82rem; border-radius: 6px;
  border-color: rgba(var(--ink), .16);
}
.row-actions button:hover:not(:disabled) { background: rgba(var(--ink), .07); }
button.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 55%, transparent); }
.row-actions button.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--danger) 10%, transparent); }

/* A disabled button that looks clickable is worse than an absent one: the
   Leave button is disabled for the household's only admin, and without this it
   read as a live control that silently did nothing. */
button:disabled { opacity: .45; cursor: not-allowed; }

/* --- owner chips ---------------------------------------------------------- */
/* Colour is assigned by sorted member label in lib/owners.js, so a person keeps
   the same colour on every screen. A chip whose colour moved would be worse
   than plain text. */
.owner-chip { display: inline-flex; align-items: center; gap: 7px; }
.owner-name { opacity: .8; }
.chip {
  width: 22px; height: 22px; flex: none; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10.5px; font-weight: 600;
}
.chip-shared { background: color-mix(in srgb, var(--accent) 24%, transparent); color: var(--pos); }
.chip-a      { background: color-mix(in srgb, var(--blue)  20%, transparent); color: var(--blue); }
.chip-b      { background: color-mix(in srgb, var(--warn)  20%, transparent); color: var(--warn); }
.chip-c      { background: color-mix(in srgb, var(--neg)   18%, transparent); color: var(--neg); }
.chip-d      { background: rgba(var(--ink), .14); color: rgba(var(--ink), .8); }
.chip-none   { background: rgba(var(--ink), .08); color: rgba(var(--ink), .5); }

.count-note { font-size: 12.5px; }
.warn-text { color: var(--warn); }

/* --- items (phone groups) ------------------------------------------------- */
.item-groups { flex-direction: column; gap: 18px; }
.item-group-head {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: 0 2px 6px;
}
.item-group-name {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em; opacity: .5;
}
.item-group-total { font-size: 12px; opacity: .4; }
.item-group-body {
  border-radius: var(--radius); background: var(--surface);
  border: 1px solid rgba(var(--ink), .09); overflow: hidden;
}
.item-row {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--row-h-touch); padding: 10px 14px;
  border-bottom: 1px solid rgba(var(--ink), .07);
  cursor: pointer; text-align: left;
}
.item-row:last-child { border-bottom: 0; }
.item-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* A paused item is still worth seeing — it is how you find the one to
   reactivate — so it dims rather than hides. */
.item-row.is-paused { opacity: .5; }

/* --- balances (phone cards) ----------------------------------------------- */
.account-cards { flex-direction: column; gap: 10px; }
.account-card {
  border-radius: var(--radius); background: var(--surface);
  border: 1px solid rgba(var(--ink), .09); padding: 14px;
}
.account-card-head { display: flex; align-items: center; gap: 11px; }
.account-card-foot {
  display: flex; align-items: center; gap: 8px; margin-top: 12px;
}
.account-card-foot > span { flex: 1; font-size: 12px; }
.account-card-foot button { min-height: 36px; }

.panel-note {
  margin-top: 20px; padding: 16px 18px; border-radius: var(--radius);
  border: 1px dashed rgba(var(--ink), .16);
  display: flex; flex-direction: column; gap: 6px;
}
.panel-note p { margin: 0; font-size: 13px; line-height: 1.5; }

/* --- entry sheet (phone) -------------------------------------------------- */
/* A <dialog> so focus trapping and page inertness come from the platform. */
.sheet {
  position: fixed; inset: auto 0 0 0; width: 100%; max-width: none;
  max-height: calc(100dvh - 52px);
  margin: 0; padding: 0; border: 0;
  border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
  background: var(--menu); color: var(--text);
  border-top: 1px solid rgba(var(--ink), .14);
  display: flex; flex-direction: column;
}
.sheet::backdrop { background: rgb(0 0 0 / .55); }
.sheet-grab { flex: none; display: flex; justify-content: center; padding: 8px 0 2px; }
.sheet-grab span { width: 38px; height: 4px; border-radius: 2px; background: rgba(var(--ink), .22); }
.sheet-head {
  flex: none; display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 6px 12px 12px;
}
.sheet-title { font-size: 16px; font-weight: 600; }
.sheet-cancel {
  min-height: var(--control-h-touch); padding: 0 8px;
  border: 0; background: none; color: rgba(var(--ink), .7); font-size: 16px;
}
.sheet-save {
  min-height: var(--control-h-touch); padding: 0 14px;
  border: 0; border-radius: 10px; font-size: 16px; font-weight: 600;
}
/* Two separate ways this collapsed on iOS, both fixed here. NO PERCENTAGE
   LENGTH may resolve against .sheet, which has a max-height and no height and
   is therefore of indefinite height.

   1. `flex: 1 1 auto`, NOT `flex: 1`. `flex: 1` expands to `flex: 1 1 0%`.
      Chrome sizes an auto-height flex container from each item's max-content
      contribution and the body rendered fine; WebKit resolves the percentage
      basis against the indefinite height as ZERO. An auto basis means "start
      from my content, then grow or shrink", which is what was meant.
   2. No `max-height: 100%`. It was added ALONGSIDE fix 1 — in the same commit,
      as belt and braces — and quietly reintroduced the identical symptom
      through the other door: Chrome follows the spec and treats a percentage
      max-height against an indefinite container as `none`, WebKit resolves it
      to zero, and a body clamped to zero looks exactly like a body with a zero
      basis. So the sheet still showed its header and its keypad with nothing
      in between, and the first fix appeared not to have worked.

   It was also redundant. .sheet's own max-height already caps the sheet, and
   `flex-shrink: 1` with `min-height: 0` already lets this shrink to fit inside
   it. It bought nothing and cost a release. */
.sheet-body {
  flex: 1 1 auto; min-height: 0;
  overflow: auto; padding: 0 16px 12px;
}
/* Pinned: with an iOS keyboard open, a footer that scrolls is a Save button
   you cannot reach. That was the original form's actual failure. */
.sheet-footer {
  flex: none; background: var(--surface);
  border-top: 1px solid rgba(var(--ink), .1);
  padding: 8px 6px calc(10px + env(safe-area-inset-bottom));
}

.amount-hero {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 10px 0 18px;
}
.amount-label {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em; opacity: .5;
}
.amount-figure strong { font-size: 44px; font-weight: 600; letter-spacing: -.03em; }
.amount-caption { font-size: 12.5px; opacity: .5; }

.field-group {
  display: flex; flex-direction: column; gap: 0;
  border-radius: var(--radius); background: var(--surface);
  border: 1px solid rgba(var(--ink), .09); overflow: hidden; margin-bottom: 12px;
}
.field-group .field {
  display: flex; align-items: center; gap: 12px; margin: 0;
  min-height: 52px; padding: 6px 14px;
  border-bottom: 1px solid rgba(var(--ink), .08);
}
.field-group .field:last-child { border-bottom: 0; }
.field-group .field > span { flex: 0 0 78px; font-size: 14.5px; opacity: .6; }
.field-group .field > :not(span) { flex: 1; min-width: 0; }

.more-details {
  border-radius: var(--radius); background: var(--surface);
  border: 1px solid rgba(var(--ink), .09); overflow: hidden;
}
.more-details summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 10px;
  min-height: 50px; padding: 0 14px; font-size: 15px;
}
.more-details summary::-webkit-details-marker { display: none; }
.more-details summary::after { content: "▸"; margin-left: auto; opacity: .45; font-size: 13px; }
.more-details[open] summary::after { content: "▾"; }
.more-hint { margin-left: auto; font-size: 12.5px; opacity: .45; }
.more-details[open] .more-hint { display: none; }
.more-body { padding: 4px 14px 14px; display: flex; flex-direction: column; gap: .6rem; }
.paid-row { min-height: var(--control-h-touch); }

/* --- keypad --------------------------------------------------------------- */
.keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.keypad-key {
  min-height: 48px; border: 0; border-radius: var(--radius-sm);
  background: rgba(var(--ink), .1); color: inherit;
  font-size: 23px; font-variant-numeric: tabular-nums;
}
.keypad-key:active { background: rgba(var(--ink), .18); }
.keypad-aux { background: transparent; font-size: 18px; }

/* --- entry dialog (desktop) ----------------------------------------------- */
dialog.entry-dialog { min-width: 640px; max-width: 92vw; }
.entry-grid { display: grid; grid-template-columns: 1fr 250px; gap: 24px; }
.entry-col { display: flex; flex-direction: column; gap: 14px; }
.amount-card {
  padding: 16px; border-radius: var(--radius);
  background: var(--surface); border: 1px solid rgba(var(--ink), .1);
  display: flex; flex-direction: column; gap: 6px;
}
/* Underline rather than a full box: the card already carries the border, and
   a second one around the figure made the number look like a form field
   rather than the headline of the dialog. Without any affordance at all the
   card read as empty, which is how this first shipped. */
.amount-input {
  border: 0; border-bottom: 1px solid rgba(var(--ink), .2);
  background: transparent; padding: 2px 0; width: 100%;
  font-size: 30px; font-weight: 600; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.amount-input::placeholder { opacity: .3; font-weight: 500; }
.amount-input:focus { outline: none; border-bottom-color: var(--accent); }

@media (max-width: 899px) {
  dialog.entry-dialog { min-width: 0; }
  .entry-grid { grid-template-columns: 1fr; }
}

/* --- reports -------------------------------------------------------------- */
.report-card {
  margin-top: 16px; padding: 20px; border-radius: var(--radius);
  background: var(--surface); border: 1px solid rgba(var(--ink), .1);
}
.report-card-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-bottom: 20px;
}
.link-button {
  margin-left: auto; border: 0; background: none;
  color: var(--pos); text-decoration: underline; text-underline-offset: 2px;
  padding: 0 4px;
}
.date-range { display: inline-flex; align-items: center; gap: 8px; }
.date-range input { color-scheme: inherit; }

/* Net per month. Plain divs with percentage heights — the prototype's own
   approach, and it removes the external chart dependency along with the class
   of bug where a stylesheet rule overrode the library's inline text colours. */
.net-chart {
  display: flex; align-items: flex-end; gap: 14px;
  height: 220px;
}
.net-col {
  flex: 1; min-width: 0; height: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  justify-content: flex-end;
}
.net-value {
  font-size: 12px; opacity: .5; white-space: nowrap;
  /* Reserves the line even when this column carries no label, so a min/max
     chart's bars stay level with an all-labels one instead of the two unlabelled
     columns growing taller than the labelled pair. */
  min-height: 1em;
}

/* --- a chart column's full figure, on tap ----------------------------------
   Compact labels made the chart readable and, on a phone, made the exact figure
   unreachable: the full one was in `title`, which needs a pointer to hover.
   Tapping a column shows it. lib/popover.js drives the dismissal, so an outside
   tap closes it, Escape closes it, the tap that closes does not activate the
   column underneath, and only one is ever open. */
.chart-col { position: relative; }
/* Transparent, and covering the whole column. A wrapping button would have put
   the bubble inside its own trigger. */
.chart-hit {
  position: absolute; inset: 0; z-index: 1;
  padding: 0; border: 0; background: none; border-radius: var(--radius-sm);
}
.chart-hit:hover { background: rgba(var(--ink), .05); }
.chart-hit:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.chart-pop {
  position: absolute; left: 0; top: calc(100% + 6px);
  min-width: max-content; max-width: 15rem; padding: 8px 10px;
  font-size: 12.5px; line-height: 1.35; text-align: left;
  border-radius: var(--radius-sm); background: var(--menu); color: var(--text);
  border: 1px solid rgba(var(--ink), .14); box-shadow: 0 12px 32px rgb(0 0 0 / .35);
}

/* The All / Min-max switch in a chart card's header. Pushed to the end of the
   row so the caption keeps its place next to the title. */
.report-card-head .label-mode { margin-left: auto; flex: none; }
.report-card-head .label-mode button { padding: 4px 10px; font-size: 12px; }
/* The track holds the bar so every column's baseline lines up; the bar grows
   upward from it. */
.net-track {
  flex: 1; width: 100%; max-width: 76px;
  display: flex; flex-direction: column; justify-content: flex-end;
}
.net-bar { width: 100%; border-radius: 6px; }
.net-bar.pos { background: var(--pos); }
.net-bar.neg { background: var(--neg); }
.net-month { font-size: 12.5px; opacity: .55; }

/* Accumulated balance, drawn around a zero line. The halves are scaled
   independently so a small dip below zero stays visible next to a large
   positive balance — the point is when the line is crossed. */
.balance-chart { display: flex; align-items: stretch; gap: 10px; }
.balance-col {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
}
.balance-up { height: 140px; width: 100%; display: flex; flex-direction: column; justify-content: flex-end; }
.balance-down { height: 60px; width: 100%; display: flex; flex-direction: column; }
.balance-zero { width: 100%; height: 1px; background: rgba(var(--ink), .22); }
.balance-bar { width: 100%; max-width: 64px; margin: 0 auto; }
.balance-bar.pos { background: var(--pos); border-radius: 5px 5px 0 0; }
.balance-bar.neg { background: var(--neg); border-radius: 0 0 5px 5px; }
.dot.neg-dot { background: var(--neg); }
.history-list .list-sub.pos { color: var(--pos); }
.history-list .list-sub.neg { color: var(--neg); }

.cat-list { display: flex; flex-direction: column; gap: 16px; }
.cat-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.cat-name { flex: 1; font-size: 14px; }
.cat-actual { font-size: 14px; font-weight: 600; }
.cat-budget { font-size: 12px; opacity: .45; width: 90px; text-align: right; }
.cat-track {
  position: relative; height: 8px; border-radius: 4px;
  background: rgba(var(--ink), .08);
}
.cat-bar {
  position: absolute; inset: 0 auto 0 0; border-radius: 4px;
  background: var(--pos);
}
.cat-bar.over { background: var(--neg); }
/* Where the budget sat, so "over" reads as a position and not only a colour —
   which also keeps it legible without relying on red/green discrimination. */
.cat-budget-mark {
  position: absolute; top: -3px; bottom: -3px; width: 2px;
  background: rgba(var(--ink), .5); border-radius: 1px;
}

@media (max-width: 899px) {
  .net-chart { gap: 6px; height: 170px; }
  .net-value { font-size: 10.5px; }
  .net-month { font-size: 11px; }
  .cat-budget { width: 70px; }
  .report-card { padding: 16px 14px; }
}

/* DO NOT add a `fill` rule for .chart svg text.
   Google Charts sets the colour inline on each <text> element from
   legend.textStyle / pieSliceTextStyle / hAxis.textStyle. A stylesheet rule
   overrides those inline values, and `fill: inherit` in particular makes the
   text inherit the <svg>'s default fill — black — so every label went dark
   while the slice colours (set as element fills, not text) still worked.
   Theme the charts through the options object in chartTheme(), not from here.

   Phase 5 removes Google Charts entirely, and this comment with it. */

/* --- household & members --------------------------------------------------- */
/* 1.5fr / 1fr on desktop, one column on a phone. The phone override is the
   @media block at the END of this section, not the shared one further up:
   `display` is the only property whose base may sit above that block, because
   the reveal group there is written to be overridden. Everything else declared
   up there is beaten by its own base rule, and grid-template-columns was —
   the phone kept the 1.5fr/1fr split, so the invite form sat in a ~150px
   column beside the members and every label inside it wrapped one word per
   line. Put household phone rules HERE. */
.household-grid {
  display: grid; grid-template-columns: 1.5fr 1fr;
  gap: 16px; align-items: start; max-width: 1100px;
}
.household-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }

.household-card {
  display: flex; align-items: center; gap: 14px;
  padding: 18px 20px; border-radius: var(--radius);
  background: var(--surface); border: 1px solid rgba(var(--ink), .1);
}
/* A square mark, not a circle: the household is a place, the people in it are
   circles. Same distinction the rail head makes. */
.household-mark {
  width: 44px; height: 44px; flex: none; border-radius: 12px;
  background: var(--accent); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px; font-weight: 600;
}
.household-id { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.household-title {
  font-size: 17px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.household-sub { font-size: 12.5px; opacity: .55; }

.person { display: inline-flex; align-items: center; gap: 10px; }
.person-name { font-weight: 500; }
.chip-pending {
  background: transparent; border: 1px dashed rgba(var(--ink), .25);
  color: rgba(var(--ink), .5);
}

.role-pill {
  display: inline-block; font-size: 12.5px; padding: 2px 9px; border-radius: 20px;
  background: rgba(var(--ink), .1); color: rgba(var(--ink), .75);
}
.role-admin {
  background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--pos);
}

/* Side column: invite and danger. */
.side-card {
  padding: 20px; border-radius: var(--radius);
  background: var(--surface); border: 1px solid rgba(var(--ink), .1);
  display: flex; flex-direction: column; gap: 12px;
}
.side-label {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; opacity: .55;
}
.side-note { font-size: 12.5px; opacity: .5; line-height: 1.5; }
.side-card input, .side-card select { width: 100%; }
/* The note explaining Member vs Admin is help text for the select above it, not
   a paragraph of its own — pulled up inside the card's 12px gap so it reads as
   attached to the field rather than floating between it and the button. */
.side-card select + .side-note { margin-top: -7px; }
.danger-card {
  background: transparent;
  border-color: color-mix(in srgb, var(--danger) 30%, transparent);
}

/* Phone list. .household-list is display:none by default — see the group
   above the breakpoint. */
.household-list-row {
  display: flex; flex-direction: column; gap: 8px;
  padding: 12px 2px; border-bottom: 1px solid rgba(var(--ink), .08);
}
.household-list-top { display: flex; align-items: center; gap: 12px; }
.household-list-row .row-actions { justify-content: flex-end; }
.person-you {
  font-size: 11.5px; padding: 1px 7px; border-radius: 20px;
  background: rgba(var(--ink), .08);
}
/* Used by the members table; see the note on the Email column. */
.cell-ellipsis {
  display: inline-block; max-width: 24ch; vertical-align: bottom;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Phone. Everything except the .household-list reveal lives here rather than in
   the shared max-width block, because that block sits ABOVE this section and
   only `display` survives being overridden from there. See the note at the top
   of the section. */
@media (max-width: 899px) {
  /* One column, and the invite form drops below the people it is about. */
  .household-grid { grid-template-columns: minmax(0, 1fr); }

  .household-card { gap: 12px; padding: 16px; }
  .household-mark { width: 40px; height: 40px; border-radius: 10px; font-size: 14px; }
  /* Settings is the card's only control and has to be thumb-sized. `flex: none`
     because the identity beside it is flex: 1 and would otherwise let the
     button be squeezed to "Settin…". */
  .household-card > button { flex: none; min-height: var(--control-h-touch); }
  /* Four dot-separated parts in ~200px: let it wrap to a second line with room
     to breathe rather than clipping a count the screen is partly about. */
  .household-sub { line-height: 1.45; }

  /* Two or three thumb-sized buttons, side by side, filling the row. Remove
     sits on the right where the row already ends, so the destructive one is
     never the button under the thumb by default. */
  .household-list-row { padding: 14px 2px; gap: 10px; }
  .household-list-row .row-actions { gap: 8px; }
  .household-list-row .row-actions button {
    flex: 1; min-height: var(--control-h-touch);
    padding: 0 .7rem; font-size: .9rem; border-radius: var(--radius-sm);
  }
  /* The role pill is the row's status and reads as cramped at table size. */
  .household-list-top .role-pill { flex: none; padding: 3px 10px; }

  .side-card { padding: 16px; }
  /* The font-size that used to live here is now the global floor at the END of
     this file — the household form was not the only place iOS zoomed, it was
     just the only place anyone had noticed. Only the touch height is local. */
  .side-card input, .side-card select, .side-card button {
    min-height: var(--control-h-touch);
  }
}

/* --- month picker ---------------------------------------------------------- */
/* Replaces <input type="month">, which Safari does not implement — see the note
   at the top of components/month-picker.js. */
.month-picker { position: relative; display: inline-block; }
.month-trigger {
  min-width: 10.5rem; text-align: left;
  display: inline-flex; align-items: center; justify-content: space-between; gap: .5rem;
}
/* The caret is a pseudo-element so the label stays the button's only text —
   `trigger.textContent = …` would otherwise wipe a real child out. */
.month-trigger::after { content: "▾"; opacity: .5; font-size: .8em; }

.month-pop {
  position: absolute; z-index: 20; top: calc(100% + 6px); left: 0;
  width: 236px; padding: 10px;
  border-radius: var(--radius); border: 1px solid rgba(var(--ink), .14);
  background: var(--menu); box-shadow: 0 12px 32px rgba(0, 0, 0, .35);
}
.month-pop-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 2px 2px 10px;
}
.month-year { font-weight: 600; font-variant-numeric: tabular-nums; }
.month-nav {
  width: 30px; height: 30px; padding: 0; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
/* minmax(0, 1fr), not 1fr: a bare 1fr track is min-width: auto, so a cell
   refuses to go below its content and the whole grid pushes past its container
   instead — which is why a narrow popover CLIPPED its third column rather than
   drawing three narrower ones. */
.month-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.month-cell { padding: .5rem .2rem; text-align: center; }
.month-cell:hover { background: rgba(var(--ink), .07); }
.month-cell.selected {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
}
/* The current month is outlined, not filled — so "where am I" and "what is
   today" stay legible at the same time, including when they are the same cell. */
.month-cell.is-now:not(.selected) { border-color: rgba(var(--ink), .45); }

/* The popover would be clipped by the sheet's own scroll container, so on a
   phone it drops to full width under the trigger rather than floating.

   It keeps the base `left: 0` at every width, including where that would put it
   past the right edge — measured 79px past at 320px in the topbar, 87px for the
   "to" picker of the generate-budget range. Two right-anchor rules used to live
   here and in .date-range to correct exactly those two cases; lib/popover.js now
   measures every panel against the viewport and nudges it in, so a popover added
   later is fitted without anyone remembering to add a third rule. */
@media (max-width: 899px) {
  .month-trigger { width: 100%; min-width: 0; }
  /* width: 100% of the trigger, but never narrower than three month cells plus
     padding. The popover is absolutely positioned, so it is free to be wider
     than the control that opened it — and it has to be, because no sharing-out
     of a 358px row guarantees the trigger enough width on its own. Belt to the
     grow rule's braces above: the topbar can be rearranged again, and a clipped
     grid should not be how we find out. */
  .month-pop { width: 100%; min-width: 15rem; }
}

/* --- bulk selection -------------------------------------------------------- */
/* Desktop only in practice: it rides on .table-card, which is hidden below
   900px, so the bar can never appear without a table above it. */
.select-cell { width: 1%; padding-right: 0 !important; }
.select-cell input { margin: 0; }

.bulk-bar {
  display: flex; align-items: center; gap: .6rem;
  margin-bottom: 10px; padding: .5rem .75rem;
  border-radius: var(--radius); background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, rgba(var(--ink), .12));
}
.bulk-count { font-weight: 600; }
.bulk-detail { font-size: .85rem; }
.bulk-spacer { flex: 1; }

.quick-dates { display: flex; gap: .4rem; margin-top: -.4rem; }

.bulk-date-list {
  display: flex; flex-direction: column; gap: .35rem;
  max-height: 240px; overflow: auto; margin-top: .5rem;
  padding-top: .5rem; border-top: 1px solid rgba(var(--ink), .1);
}
.bulk-date-row { display: flex; align-items: center; gap: .6rem; }
.bulk-date-label {
  flex: 1; min-width: 0; font-size: .9rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* --- weekday chips --------------------------------------------------------- */
/* Names, never numbers: the ISO-vs-spreadsheet numbering is what put five
   weekly items a day late for months. See lib/weekdays.js. */
.repeats { display: inline-flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.day-chip {
  font-size: 11.5px; padding: 1px 7px; border-radius: 20px;
  background: color-mix(in srgb, var(--blue) 18%, transparent); color: var(--blue);
}
.day-chip.dim { background: rgba(var(--ink), .08); color: rgba(var(--ink), .5); }

/* Weekday multi-select in the item form. Checkboxes, not <select multiple>:
   clicking a second option in a multiple select REPLACES the first unless you
   know to ctrl-click, which silently drops a day from a schedule. */
.weekday-picker { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px 10px; }
.weekday-option {
  display: flex; align-items: center; gap: .5rem;
  padding: .25rem .3rem; border-radius: var(--radius-sm); cursor: pointer;
}
.weekday-option:hover { background: rgba(var(--ink), .06); }
.weekday-option input { margin: 0; }
@media (max-width: 899px) {
  .weekday-picker { grid-template-columns: minmax(0, 1fr); }
  .weekday-option { min-height: var(--control-h-touch); }
}

/* --- search box ------------------------------------------------------------ */
/* One component for Month and Items. The clear button is ours rather than
   type=search's native affordance: WebKit only draws that while the field is
   focused AND non-empty, Firefox draws none, and none of them can be given a
   thumb-sized hit area. */
.search-box { position: relative; display: inline-flex; flex: 1 1 auto; min-width: 0; max-width: 280px; }
.search-input { width: 100%; height: 36px; padding-right: 2rem; }
/* The engine's own clear button would sit underneath ours. */
.search-input::-webkit-search-cancel-button { display: none; }
.search-clear {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 26px; height: 26px; padding: 0; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: rgba(var(--ink), .55);
  font-size: 19px; border-radius: 50%;
}
.search-clear:hover { background: rgba(var(--ink), .1); color: var(--text); }

/* Month's multi-select toggle. */
.select-toggle {
  flex: none; width: 36px; height: 36px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  color: rgba(var(--ink), .6);
}
.select-toggle:hover { color: var(--text); }
.select-toggle[aria-pressed="true"] {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--text);
}

@media (max-width: 899px) {
  .search-box { max-width: none; }
  .search-input { height: var(--control-h-touch); }
  .select-toggle { width: var(--control-h-touch); height: var(--control-h-touch); }
}

/* Repeats column. Two shapes: weekly is a set of days, everything dated is one
   date — so days get pills and a date gets a dot. */
.repeats-freq { white-space: nowrap; }
.repeats-dot {
  width: 4px; height: 4px; flex: none; border-radius: 50%;
  background: rgba(var(--ink), .35); margin: 0 1px;
}
.repeats-when { font-variant-numeric: tabular-nums; opacity: .75; white-space: nowrap; }

/* --- sorting --------------------------------------------------------------- */
.sort-control { gap: .4rem; }
.sort-select { height: 36px; max-width: 12rem; }
.sort-dir {
  width: 36px; height: 36px; padding: 0; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px;
}
/* A header that sorts is a button, but must not LOOK like one — it is still a
   column heading, and a row of eight buttons reads as a toolbar. */
.sort-head {
  border: 0; background: none; padding: 0; margin: 0;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
  display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
}
.sort-head:hover { color: var(--text); }
.sort-head.active { color: var(--text); font-weight: 700; }
.sort-arrow { font-size: .9em; }
@media (max-width: 899px) {
  .sort-select { height: var(--control-h-touch); flex: 1; max-width: none; }
  .sort-dir { width: var(--control-h-touch); height: var(--control-h-touch); }
  .sort-control { flex: 1 1 100%; }
}

/* --- Month's filter bar, on a phone ---------------------------------------

   One wrapping flex row put SIX ragged rows on a 393px screen: three segmented
   groups each on their own line, "All types" broken across two lines inside a
   28px-tall button and clipped by it, and the multi-select toggle stranded alone
   on a row of its own. A grid instead, so every control has a declared home:

     row 1   status                    (full width)
     row 2   type                      (full width)
     row 3   search            toggle
     row 4   scope     sort by

   393px: 4 rows, 200px tall, nothing clipped, every target 44px.
   320px: the same 4 rows; only the type track needs to scroll.

   Placed HERE, at the end of the sorting section, rather than in the shared
   phone block ~750 lines above — the bases this overrides (.segmented button,
   .select-toggle, .sort-control) are all declared between the two, and at equal
   specificity a later rule wins at every width. web/test/css-order.mjs is the
   guard for exactly that mistake.

   SCOPED to .screen-month as of phase 0.2, and that is the point of the phase.
   The grid below is designed around Month's five controls, but `.filter-bar` is
   a class every screen uses, so it was placing — and misplacing — bars it knows
   nothing about:

     Items    has two children and no areas of its own, so owner and count
              auto-placed into row 1 while `> .sort-control` pinned the sorter to
              row 4, leaving a visibly empty row 3 between them.
     Forecast has a four-key segmented and a long caption. Column 1 is `auto`, so
              the segmented took the row and the caption was squeezed into ~110px
              and wrapped one word per line, eleven lines tall — with the
              segmented floating in the middle of the void that created.
     Overview the same, plus two date inputs when Custom is on.

   Those three now fall back to the base `.filter-bar` flex row, which wraps and
   is a strict improvement on all three. Phases 4 and 7 give Items, Forecast and
   Overview grids of their own, under their own screen classes.

   The screen classes already exist on each renderer's root element:
   .screen-month (screens/month.js), .screen-items (screens/items.js),
   .screen-forecast and .screen-reports (screens/reports.js).

   Note what is deliberately NOT scoped: the .segmented and .sort-control rules
   at the bottom of this block. Those are phone sizing, not Month layout, and
   every screen wants them. */
@media (max-width: 899px) {
  .screen-month .filter-bar {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 8px;
  }
  /* Four rows became two. The status and type segmented controls and the search
     box are all inside the filter panel since phase 2, which is most of what
     this grid used to be arranging. What is left is the scope switch, the
     button that opens the panel, the multi-select toggle and the sorter:

       row 1   By item | All rows        [▽ 2]  [☑]
       row 2   sort by                          (full width)

     The chip row is a SIBLING of the bar, not a cell in it — chips wrap to as
     many lines as they need, and a grid row sized to fit three of them would
     leave a gap when there is one. */
  .screen-month .filter-bar > .filter-scope    { grid-area: 1 / 1; }
  .screen-month .filter-bar > .filter-open-wrap { grid-area: 1 / 2; justify-self: end; }
  .screen-month .filter-bar > .select-toggle   { grid-area: 1 / 3; }
  .screen-month .filter-bar > .sort-control    { grid-area: 2 / 1 / auto / -1; }

  /* A segmented key never breaks its label across two lines inside a fixed
     height. When the keys genuinely cannot fit — four types on a 320px screen —
     the track scrolls sideways, which loses nothing, where wrapping hid the
     second half of the word behind the button's own edge. */
  .filter-bar .segmented {
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .filter-bar .segmented::-webkit-scrollbar { display: none; }
  .filter-bar .segmented button {
    height: 38px;              /* + 2 x 3px of track padding = the 44px touch target */
    padding: 0 8px;
    white-space: nowrap;
    flex: 1 0 auto;            /* share the width when there is room, never shrink below the label */
  }

  .filter-bar .sort-control {
    display: flex; align-items: center; justify-content: flex-end; min-width: 0;
  }
  .filter-bar .sort-select { min-width: 0; }
  /* The select's own default option already reads "Default order"; the label was
     spending 60px of a 361px row to repeat a word the control says itself. */
  .filter-bar .sort-control .control-label { display: none; }
}

/* A forecast month past the generated horizon. Dimmed rather than hidden: the
   month is real and part of the horizon you asked for, it just has nothing
   planned in it — and a net of 0.00 drawn like any other month reads as "no
   money moves", which is a different claim entirely. */
.list-row.is-unbudgeted { opacity: .5; }
.list-row.is-unbudgeted .list-amount { font-weight: 400; }

.panel-note.warn-note {
  border-color: color-mix(in srgb, var(--warn) 35%, transparent);
  background: color-mix(in srgb, var(--warn) 8%, transparent);
}

/* --- popover --------------------------------------------------------------- */
/* lib/popover.js adds .pop-panel to every panel it drives. It positions nothing
   — that stays with the panel's own rule (.month-pop, .overflow-menu) — so all
   this owns is stacking, and the bottom-sheet presentation below 640px. */
/* Above the topbar (2) and above .month-pop's own 20, deliberately: a panel that
   slides under the chrome that opened it is worse than no panel. This rule is
   near the end of the file, so it wins against a panel's own z-index at equal
   specificity — which is the intent, not an accident. */
.pop-panel { z-index: 50; }

@media (max-width: 639px) {
  /* A panel wide enough to be worth a popover on a desktop is a panel that runs
     off the side of a phone. Anchored becomes pinned to the bottom, full width,
     where the thumb already is. */
  .pop-panel.is-sheet {
    position: fixed;
    inset: auto 0 0 0;
    top: auto;
    width: auto;
    max-height: 70dvh;
    overflow-y: auto;
    border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
    padding-bottom: max(12px, env(safe-area-inset-bottom));
    box-shadow: 0 -12px 40px rgb(0 0 0 / .45);
  }
  .pop-panel.is-sheet button { min-height: var(--control-h-touch); }
}

/* --- filter panel ---------------------------------------------------------- */
/* The funnel button, its count badge, the panel, and the chip row that keeps an
   off-screen filter honest. Declared after .pop-panel above, because
   .filter-panel needs to beat `.pop-panel.is-sheet { overflow-y: auto }` — the
   sheet must not scroll as a whole or the "Show N items" button scrolls away
   with it. The body scrolls; the footer stays. */

.filter-open-wrap { position: relative; display: inline-flex; flex: none; }
.filter-open {
  position: relative;
  width: 36px; height: 36px; padding: 0; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  color: rgba(var(--ink), .6);
}
.filter-open:hover { color: var(--text); }
.filter-open.is-active,
.filter-open[aria-expanded="true"] {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--text);
}
.filter-badge {
  position: absolute; top: -5px; right: -5px;
  min-width: 16px; height: 16px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 8px; background: var(--accent); color: #fff;
  font-size: 10.5px; font-weight: 600; line-height: 1;
}

.filter-panel {
  /* Anchored LEFT, because on desktop the filter button sits near the left of
     the bar — scope, filter, select, sort — and `right: 0` opened the panel
     backwards across the content and under the 244px rail, clipping the first
     third of every label. The phone grid puts the button on the RIGHT instead,
     so it flips below 900px; see the override further down. Anything narrower
     than 640px is a sheet and ignores both. */
  position: absolute; left: 0; top: calc(100% + 6px);
  width: 300px; max-height: 70vh;
  display: flex; flex-direction: column; overflow: hidden;
  border-radius: var(--radius); background: var(--menu);
  border: 1px solid rgba(var(--ink), .14); box-shadow: 0 16px 44px rgb(0 0 0 / .45);
}
.filter-panel-head {
  padding: 12px 14px 8px; border-bottom: 1px solid rgba(var(--ink), .09);
}
.filter-panel-title { font-weight: 600; font-size: 14px; }
.filter-panel-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px 14px 12px; }
.filter-panel-foot {
  flex: none; display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 10px 14px;
  border-top: 1px solid rgba(var(--ink), .09); background: var(--menu);
}
.filter-apply { flex: 1 1 auto; max-width: 60%; }
.filter-clear:disabled { opacity: .35; text-decoration: none; cursor: default; }

.filter-group { padding: 12px 0 2px; }
.filter-group + .filter-group { border-top: 1px solid rgba(var(--ink), .07); }
.filter-group-label {
  display: block; margin-bottom: 6px;
  font-size: 11px; text-transform: uppercase; letter-spacing: .05em; opacity: .55;
}
.filter-option {
  display: flex; align-items: center; gap: 9px;
  min-height: 32px; cursor: pointer; border-radius: var(--radius-sm);
}
.filter-option:hover { background: rgba(var(--ink), .05); }
.filter-option input { flex: none; margin: 0; }
.filter-option-label { flex: 1 1 auto; min-width: 0; }
/* Tabular so the column of counts does not jitter as the draft changes. */
.filter-option-count { flex: none; opacity: .5; font-size: 12.5px; font-variant-numeric: tabular-nums; }
/* Matches nothing under the current draft — dimmed, never removed. A checkbox
   that disappears as you tick its neighbour makes the panel feel unstable. */
.filter-option.is-empty { opacity: .4; }
.filter-days { display: flex; align-items: center; gap: 8px; }
.filter-day { width: 72px; text-align: center; }
.filter-search { width: 100%; }

/* --- filter chips ---------------------------------------------------------- */
/* The only thing on screen that says why the list is short, once the filters
   themselves are behind a button. */
.filter-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: -6px 0 14px; }
.filter-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 8px 3px 10px; font-size: 12.5px; line-height: 1.6;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-color: color-mix(in srgb, var(--accent) 34%, transparent);
}
.filter-chip:hover { background: color-mix(in srgb, var(--accent) 26%, transparent); }
.filter-chip-x { opacity: .6; font-size: 14px; }
.filter-chip.is-clear {
  background: transparent; border-color: rgba(var(--ink), .16); opacity: .8;
}

@media (max-width: 899px) {
  /* The grid right-aligns the button in its column, so the panel has to open
     back towards the middle rather than off the right edge. */
  .filter-panel { left: auto; right: 0; }
  .filter-open { width: var(--control-h-touch); height: var(--control-h-touch); }
  .filter-option { min-height: var(--control-h-touch); }
  .filter-chip { padding: 6px 10px 6px 12px; }
}

@media (max-width: 639px) {
  /* As a bottom sheet the panel is already full width and pinned; these undo
     the anchored geometry rather than adding to it. This is the one panel the
     sheet presentation is FOR — six sections anchored to a 44px button at the
     right edge of a 390px screen would be unusable. */
  .filter-panel.is-sheet { width: auto; max-height: 78dvh; }
  .filter-panel.is-sheet .filter-panel-body { padding: 4px 16px 12px; }
  .filter-panel.is-sheet .filter-panel-foot { padding: 12px 16px; }
}

/* --- compact sort menu ----------------------------------------------------- */
/* Items' sorter. sortControl's select-plus-arrow is ~200px, which on a 320px
   screen was taking a row of its own and leaving an empty one above it. */
.sort-menu-wrap { position: relative; display: inline-flex; flex: none; }
.sort-menu-open {
  width: 36px; height: 36px; padding: 0; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  color: rgba(var(--ink), .6); font-size: 15px;
}
.sort-menu-open:hover { color: var(--text); }
.sort-menu-open.is-active,
.sort-menu-open[aria-expanded="true"] {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--text);
}
.sort-menu {
  position: absolute; left: 0; top: calc(100% + 6px);
  min-width: 232px; max-height: 60vh; overflow-y: auto;
  padding: 6px; display: flex; flex-direction: column; gap: 2px;
  border-radius: var(--radius); background: var(--menu);
  border: 1px solid rgba(var(--ink), .14); box-shadow: 0 16px 44px rgb(0 0 0 / .45);
}
.sort-menu-item {
  border: 0; background: none; text-align: left; white-space: nowrap;
  min-height: 36px; padding: 0 10px; border-radius: var(--radius-sm);
}
.sort-menu-item:hover { background: rgba(var(--ink), .07); }
.sort-menu-item[aria-checked="true"] { background: var(--seg); font-weight: 600; }

/* --- Items' toolbar, on a phone -------------------------------------------
   The three rows the screen actually wants:

     row 1   New item                    [▽]  [⇅]
     row 2   search                                  (full width)
     row 3   All | Shared | Alex | Lari    35 of 131 (the .filter-bar, below)

   Rows 1 and 2 are .panel-actions and row 3 is .filter-bar, which are two
   separate elements — .panel-actions is static markup so the search box can
   survive a repaint, and .filter-bar is rendered by screens/items.js. That is
   why this is two rules and not one grid.

   Placed at the END of the file, after .sort-menu-open and .filter-open above
   and after .panel-actions' own phone rule ~900 lines up. web/test/
   css-order.mjs guards the general form of that mistake. */
@media (max-width: 899px) {
  .panel[data-panel="items"] .panel-actions {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 8px;
  }
  .panel[data-panel="items"] #new-item        { grid-area: 1 / 1; }
  .panel[data-panel="items"] #items-filter-field { grid-area: 1 / 3; }
  .panel[data-panel="items"] #items-sort-field   { grid-area: 1 / 4; }
  .panel[data-panel="items"] #items-search-field { grid-area: 2 / 1 / auto / -1; }
  .panel[data-panel="items"] #new-item { min-height: var(--control-h-touch); }
  /* The mounts are plain divs, and .search-box is inline-flex — so without this
     the field sizes to the input's default ~20 characters and sits at 222px in
     a 358px row, which looks like the old cramped toolbar rather than the
     full-width row it is. */
  .panel[data-panel="items"] #items-search-field { display: flex; }
  .panel[data-panel="items"] #items-search-field .search-box { flex: 1 1 auto; }

  .screen-items .filter-bar {
    display: flex; flex-wrap: nowrap; align-items: center; gap: 10px;
  }
  /* The owner track scrolls inside itself if four owners will not fit, rather
     than pushing the count off the row. */
  .screen-items .filter-bar > .segmented { min-width: 0; }
  .screen-items .filter-bar > .count-note { flex: none; margin-left: auto; text-align: right; }

  .sort-menu-open { width: var(--control-h-touch); height: var(--control-h-touch); }
  /* Right-aligned in the grid, so the menu opens back towards the middle. */
  .sort-menu { left: auto; right: 0; }
  .sort-menu-item { min-height: var(--control-h-touch); }
}

/* --- Month's toolbar, on a phone -------------------------------------------
   Two rows, the same shape Items' toolbar has above:

     row 1   Add entry   Generate budget…        [⋯]
     row 2   search                                     (full width)

   The search box could not go on .filter-bar, which is already a grid one
   screen-block up and would have been the tidier home: that bar is rendered by
   screens/month.js into #month-body, which is replaced on every repaint, and a
   search field repaints on every keystroke-pause. .panel-actions is static
   markup, which is the whole reason the box can live there and keep its caret.

   .panel-actions' base phone rule sets `flex-wrap: nowrap` ~1200 lines up —
   that is what stopped this row wrapping to two lines and started it scrolling
   sideways instead. A grid replaces the flex row outright rather than fighting
   it, so nothing here depends on beating that rule at equal specificity; it
   depends on being LATER in the file, which is what web/test/css-order.mjs
   checks. Adding a fourth control to row 1 without a matching column is how the
   80px overflow comes back. */
@media (max-width: 899px) {
  .panel[data-panel="month"] .panel-actions {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 8px;
  }
  .panel[data-panel="month"] #new-entry { grid-area: 1 / 1; }
  .panel[data-panel="month"] #generate  { grid-area: 1 / 2; }
  .panel[data-panel="month"] .overflow  { grid-area: 1 / 3; }
  .panel[data-panel="month"] #month-search-field { grid-area: 2 / 1 / auto / -1; }
  .panel[data-panel="month"] #new-entry,
  .panel[data-panel="month"] #generate { min-height: var(--control-h-touch); }
  /* "Generate budget…" is 152px of label. Its 1fr column is 202px at 390 and
     132px at 320, so at 320 it MUST shrink — and a button will not on its own:
     min-width defaults to auto, and .panel-actions' phone rule adds
     white-space: nowrap. Left alone it kept its intrinsic width and ran 13px
     underneath the ⋯ button. Before this grid the same 152px overflowed the
     flex row sideways instead, which is phase 3's horizontal scroll turning up
     again one layout later.

     So: allowed to shrink to its column, and ellipsis rather than clip, so what
     survives still reads as a truncated label rather than a rendering fault. */
  .panel[data-panel="month"] #generate {
    min-width: 0; overflow: hidden; text-overflow: ellipsis;
  }
  /* The mount is a plain div and .search-box is inline-flex, so without this it
     sizes to the input's default ~20 characters and sits mid-row looking like a
     control rather than the full-width field it is. Same fix as Items'. */
  .panel[data-panel="month"] #month-search-field { display: flex; }
  .panel[data-panel="month"] #month-search-field .search-box { flex: 1 1 auto; }
}

/* --- Reports' bars, on a phone ---------------------------------------------

   Both were being laid out by Month's grid until phase 0.2 scoped it away, and
   the base flex row they fell back to is an improvement but still not right:
   the horizon has four keys and the caption is a sentence, so on one row the
   caption gets whatever is left — about 110px — and wraps one word per line.

   Two full-width rows instead, three when Custom is on:

     row 1   6 months | 12 months | End of year | Custom
     row 2   Up to · March 2027                          (Custom only)
     row 3   August + 6 months · 2 not budgeted yet

   Declared at the END of the file, after .segmented and .sort-control, which is
   the ordering convention web/test/css-order.mjs guards. */
@media (max-width: 899px) {
  .screen-forecast .filter-bar,
  .screen-reports .filter-bar {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: center;
    gap: 8px;
  }
  /* The tracks scroll inside themselves rather than forcing the row wider —
     four keys do not fit at 320px and never will. */
  .screen-forecast .filter-bar > .filter-horizon,
  .screen-reports .filter-bar > .filter-period { min-width: 0; }
  /* Two date inputs plus a segmented cannot share a row at 320px: Safari gives
     a date input ~150px minimum, and the 16px floor widened them further. */
  .screen-reports .filter-bar > .date-range { display: flex; gap: 8px; }
  .screen-reports .filter-bar > .date-range input { flex: 1 1 0; min-width: 0; }
  .screen-forecast .filter-bar > .filter-upto { justify-content: flex-start; }
  /* The caption is a sentence and gets a row, not a leftover column. */
  .screen-forecast .filter-bar > .count-note,
  .screen-reports .filter-bar > .count-note { text-align: left; }

  /* Same fault one level down: a card head is a label and a sentence side by
     side, and at 390px "bars above the line are money in the bank" wraps back
     across "ACCUMULATED BALANCE". Baseline alignment is what makes them collide
     rather than merely crowd. Stack them. */
  .report-card-head {
    flex-direction: column; align-items: flex-start;
    gap: 2px; margin-bottom: 14px;
  }
  .report-card-head .muted { font-size: 12.5px; line-height: 1.35; }
  /* The head is a column on a phone, so margin-left: auto has nothing to push
     against and the switch would sit full width under the caption. */
  .report-card-head .label-mode { margin-left: 0; align-self: flex-start; }
  /* Except where the second child is a control rather than a caption — a button
     under a heading reads as a list item, not an action. */
  .report-card-head:has(> button) { flex-direction: row; align-items: baseline; }
}

/* --- the month already under way -------------------------------------------
   Month zero holds only what is LEFT to pay, so it is not comparable with the
   full months beside it. Hatched rather than merely dimmed: dimming is what
   .is-unbudgeted uses for "nothing planned here", and these two must not read
   as the same state — one is a settled month, the other a missing one. */
.balance-col.is-partial .balance-bar {
  background-image: repeating-linear-gradient(
    135deg, transparent 0 3px, rgba(var(--ink), .28) 3px 6px);
}
.balance-col.is-partial .net-month { font-style: italic; opacity: .8; }
.list-row.is-partial .list-title { font-style: italic; }
.list-row.is-partial { border-left: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); padding-left: 8px; }
/* The staleness line is a caveat, not an error: it says the opening balance may
   be older than this month's payments, which is worth reading and not worth
   alarming about. */
.note.warn { color: var(--warn); opacity: .9; }

/* --- generate-budget preview ------------------------------------------------ */
/* The plan, month by month. Only rendered for a multi-month range — for one
   period the totals above already are the breakdown. */
.plan-periods { margin: .6rem 0 .2rem; max-height: 40vh; overflow: auto; }
.plan-periods table { font-size: .88rem; }
.plan-periods th.num, .plan-periods td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* A month that needs nothing is the thing you most want to spot in a six-month
   run, and it is otherwise invisible inside the total. */
.plan-periods tr.is-noop { opacity: .45; }
.range-summary { margin: .1rem 0 .8rem; }
.date-range { display: flex; align-items: center; gap: .5rem; }
@media (max-width: 899px) {
  .date-range { flex-wrap: wrap; }
  .date-range > .month-picker { flex: 1 1 40%; min-width: 0; }
}

/* --- the 16px input floor -------------------------------------------------- */
/* iOS Safari zooms the whole page in when a focused field is under 16px, and it
   does not zoom back out when the keyboard closes — you are left on a page that
   is scrolled sideways, which reads as a broken layout rather than as zoom. The
   body is 15px (see the `font` shorthand near the top), so EVERY inherited
   control was one pixel under the threshold. This is the third of the three zoom
   mechanisms; lib/viewport.js has the other two.

   Buttons are excluded on purpose: iOS only zooms for fields that take text, and
   .segmented button depends on its 13px to fit four keys on a 320px screen.

   Placed at the very END of the file, and it has to stay here. `button, select,
   input { font: inherit }` sits at ~line 590 and .side-card's own rule at ~1130;
   at equal specificity the later declaration wins, so anywhere above those two
   this rule would silently do nothing. web/test/css-order.mjs guards the general
   form of that mistake. */
@media (max-width: 899px) {
  input, select, textarea { font-size: 16px; }
}

/* --- environment banner (dev plan §5d) ------------------------------------- */
/* A thin fixed strip, deliberately loud. Created by app.js only when
   /api/health reports a non-prod environment, so prod never renders it. */
#env-banner {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  padding: 2px 0 calc(2px + env(safe-area-inset-top));
  text-align: center; font-size: .7rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
  background: var(--warn); color: #1a1a18;
}
