/* ============================================================
   EarthStars — app.css
   Extracted from UI_win12.html (design reference — do not edit that file).
   Sections:
     1. Design tokens
     2. Base
     3. App shell (app bar, sidebar/drawer, content)
     4. Components (card, row, table, menu, iconbtn)
     5. Forms & buttons
     6. Feedback (alert, badge, empty state, pagination)
     7. Auth screen
     8. Desktop (>= 860px)
     9. Reduced motion / print
   Never hardcode a colour OR a font-size — always reference var(--...).
   Typography (font stack, size scale, line-heights, weights) follows
   Outlook Web / Microsoft 365 — the Fluent 2 type ramp — NOT UI_win12.html.
   See CLAUDE.md, heading "Typography".
   ============================================================ */

/* ============================================================
   1. DESIGN TOKENS
   ============================================================ */
:root{
  /* Surfaces */
  --surface:#ffffff;
  --surface-alt:#f3f7fc;
  /* Card surfaces are SOLID white, not frosted — see CLAUDE.md "Design System" */
  --card-solid:#ffffff;
  --card:var(--card-solid);
  --flyout:rgba(249,251,253,.92);

  /* Text */
  --text:#1b1b1b;
  --text-2:#5c6570;
  --text-3:#8b949e;

  /* Lines & states */
  --stroke:rgba(0,0,0,.07);
  --stroke-strong:rgba(0,0,0,.12);
  --hover:rgba(0,0,0,.04);
  --selected:rgba(0,120,212,.10);
  --focus-ring:rgba(15,108,189,.12);

  /* Scrollbar */
  --scroll-thumb:rgba(0,0,0,.24);
  --scroll-thumb-hover:rgba(0,0,0,.40);

  /* Brand */
  --accent:#0f6cbd;
  --accent-soft:#4aa3f0;
  --accent-contrast:#ffffff;

  /* Semantic */
  --ok:#107c41;
  --ok-bg:rgba(16,124,65,.10);
  --warn:#9a6700;
  --warn-bg:rgba(224,168,0,.14);
  --danger:#c4314b;
  --danger-bg:rgba(196,49,75,.10);
  /* Owing: past "fine", not yet "late" — between muted and danger. */
  --due:#b8501c;
  --due-bg:rgba(184,80,28,.12);
  --info:#0f6cbd;
  --info-bg:rgba(15,108,189,.10);

  /* Typography — reference: Outlook Web / Microsoft 365 (Fluent 2 type ramp).
     System fonts only, no web font. "Segoe UI" (not the Variable cut) is what
     Outlook itself asks for; Leelawadee UI is the Windows Thai face that Segoe
     falls back to, so Thai matches Outlook too.
     The ramp has NO 13px step — every size below is a real Fluent step, and
     each one comes with its paired line-height. Weights: 400 / 600 only. */
  --font:"Segoe UI","Segoe UI Web (West European)","Leelawadee UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue","IBM Plex Sans Thai","Noto Sans Thai",sans-serif;
  --fs-caption:12px;   --lh-caption:16px;   /* caption1  — hint, table head, badge */
  --fs-body:14px;      --lh-body:20px;      /* body1     — BASE: body, input, label, table, row */
  --fs-h2:16px;        --lh-h2:22px;        /* subtitle2 — card / section heading */
  --fs-title:20px;     --lh-title:26px;     /* subtitle1 — page title */
  --fs-stat:24px;      --lh-stat:32px;      /* title3    — dashboard stat number */
  --fs-btn:14px;       /* button label (Fluent button medium) */
  --fs-btn-sm:12px;    /* small / in-table button */

  /* Shape / motion */
  --r-card:10px;
  --r-row:6px;
  --r-field:6px;
  --field-h:40px;      /* input / select / button height */
  --ease:cubic-bezier(.2,.9,.25,1);
  --dur:280ms;

  /* Layout */
  --sidebar-w:250px;
  --bp:860px;

  --shadow-card:0 1px 2px rgba(0,0,0,.06);
  --shadow-flyout:8px 0 28px rgba(20,40,70,.22);

  /* App bar — frosted glass */
  --appbar-bg:rgba(255,255,255,.25);
  --appbar-shadow:0 10px 22px -14px rgba(20,40,70,.35);
  --appbar-border:rgba(0,0,0,.10);
  --appbar-highlight:rgba(255,255,255,.65);
}

[data-theme="dark"]{
  --surface:#1c1c1c;
  --surface-alt:#202020;
  --card-solid:#2b2b2b;
  --card:var(--card-solid);
  --flyout:rgba(38,38,38,.92);
  --text:#f3f3f3;
  --text-2:#c8c8c8;
  --text-3:#9a9a9a;
  --stroke:rgba(255,255,255,.08);
  --stroke-strong:rgba(255,255,255,.14);
  --hover:rgba(255,255,255,.06);
  --selected:rgba(96,175,255,.16);
  --focus-ring:rgba(96,175,255,.20);
  --scroll-thumb:rgba(255,255,255,.24);
  --scroll-thumb-hover:rgba(255,255,255,.42);
  --accent:#60afff;
  --accent-soft:#9ecbff;
  --accent-contrast:#10243a;
  --ok:#6ccb8f;
  --ok-bg:rgba(108,203,143,.14);
  --warn:#e8c05a;
  --warn-bg:rgba(232,192,90,.14);
  --danger:#ff8098;
  --danger-bg:rgba(255,128,152,.14);
  --due:#f0a068;
  --due-bg:rgba(240,160,104,.14);
  --info:#60afff;
  --info-bg:rgba(96,175,255,.14);
  --shadow-card:0 1px 2px rgba(0,0,0,.4);
  --appbar-bg:rgba(32,32,32,.28);
  --appbar-shadow:0 10px 24px -14px rgba(0,0,0,.6);
  --appbar-border:rgba(255,255,255,.12);
  --appbar-highlight:rgba(255,255,255,.08);
}

/* ============================================================
   2. BASE
   ============================================================ */
*{box-sizing:border-box;margin:0;padding:0}

/* The browser's own [hidden]{display:none} is a user-agent rule, so ANY class
   here that sets display beats it: .field{display:flex} kept the rate field on
   screen after both the server and app.js had marked it hidden, and .alert did
   the same to a notice banner. Adding a per-component rule each time is how
   that keeps happening, so this is stated once for everything.
   !important because the whole point is to outrank whatever display the
   element's own class sets. */
[hidden]{display:none!important}

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

body{
  min-height:100vh;min-height:100dvh;
  overflow-x:hidden;
  font-family:var(--font);
  font-size:var(--fs-body);
  line-height:var(--lh-body);
  -webkit-font-smoothing:antialiased;
  color:var(--text);
  background:var(--surface-alt);
}

a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{
  outline:2px solid var(--accent);outline-offset:1px;
}

.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;
}

/* ---------- Scrollbars ----------
   Windows' native bar (chunky, with arrow buttons) reads far heavier than the
   rest of this UI, and it shows up inside the modal, the data tables, the
   drawer and the dropdown menus. One slim treatment for all of them.
   scrollbar-width is NOT inherited, hence the universal selector. */
*{scrollbar-width:thin;scrollbar-color:var(--scroll-thumb) transparent}

::-webkit-scrollbar{width:12px;height:12px}
::-webkit-scrollbar-track,
::-webkit-scrollbar-corner{background:transparent}
::-webkit-scrollbar-button{display:none}      /* no arrow buttons */
::-webkit-scrollbar-thumb{
  background:var(--scroll-thumb);
  border:4px solid transparent;               /* inset: a 4px bar in a 12px gutter */
  border-radius:99px;
  background-clip:content-box;
}
::-webkit-scrollbar-thumb:hover{
  background:var(--scroll-thumb-hover);
  background-clip:content-box;
}

/* ============================================================
   3. APP SHELL — mobile-first
   ============================================================ */
.app{
  position:relative;
  width:100%;
  min-height:100vh;min-height:100dvh;
  background:var(--surface-alt);
  overflow-x:clip;
  display:flex;flex-direction:column;
  isolation:isolate;
}

/* ---------- App bar ---------- */
.appbar{
  position:sticky;top:8px;z-index:10;
  flex:0 0 auto;
  display:flex;align-items:center;gap:4px;
  margin:8px;
  padding:6px 8px;
  background:var(--appbar-bg);
  -webkit-backdrop-filter:blur(7px) saturate(140%);
  backdrop-filter:blur(7px) saturate(140%);
  border:1px solid var(--appbar-border);
  border-radius:16px;
  box-shadow:var(--appbar-shadow), inset 0 1px 0 var(--appbar-highlight);
}

.iconbtn{
  width:30px;height:30px;flex:0 0 auto;
  display:grid;place-items:center;
  border:0;border-radius:var(--r-row);
  background:transparent;color:var(--text-2);
  cursor:pointer;
  transition:background 140ms var(--ease), color 140ms var(--ease);
}
.iconbtn:hover{background:var(--hover)}
.iconbtn:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.iconbtn[disabled]{opacity:.35;pointer-events:none}

/* Tinted action buttons. Every icon in a table's action column used to be the
   same grey, so "view" and "delete" read as one undifferentiated pair and the
   tooltip was the only way to tell them apart. The tone says what the button
   does before it is hovered, and the hover background is tinted to match so
   the colour is not swallowed the moment the pointer lands on it.
   Tones carry meaning, not decoration -- keep the mapping:
     .accent  reversible / read-only: view, edit, print, copy, reset password
     .ok      money in, confirm, approve
     .warn    money out, adjust or reverse a balance, waiting on approval
     .danger  irreversible: delete, cancel, revoke, unlink
   Anything that does not fit stays grey (the "more actions" menu, close, the
   hamburger) -- a colour on everything is the same as a colour on nothing. */
.iconbtn.accent{color:var(--accent)}
.iconbtn.accent:hover{background:var(--info-bg)}
.iconbtn.ok{color:var(--ok)}
.iconbtn.ok:hover{background:var(--ok-bg)}
.iconbtn.warn{color:var(--warn)}
.iconbtn.warn:hover{background:var(--warn-bg)}
.iconbtn.danger{color:var(--danger)}
.iconbtn.danger:hover{background:var(--danger-bg)}
.desk-only{display:none}

/* ☰ turns into a red × while the drawer is open */
.hamburger .ic-close{display:none;color:var(--danger)}
.app[data-drawer="open"] .hamburger .ic-menu{display:none}
.app[data-drawer="open"] .hamburger .ic-close{display:block}

.breadcrumb{
  flex:1 1 auto;min-width:0;
  display:flex;align-items:center;gap:7px;
  font-size:var(--fs-body);color:var(--text);
  padding-left:2px;
}
.breadcrumb span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.breadcrumb .sep{color:var(--text-3);flex:0 0 auto}

/* Village badge in the app bar */
.village-chip{
  flex:0 0 auto;
  display:inline-flex;align-items:center;gap:6px;
  max-width:42vw;
  padding:4px 9px;
  font-size:var(--fs-caption);font-weight:600;
  color:var(--accent);
  background:var(--selected);
  border:1px solid var(--stroke-strong);
  border-radius:99px;
  cursor:pointer;
}
.village-chip .code{font-variant-numeric:tabular-nums;opacity:.85;font-weight:400}
.village-chip .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ---------- Shell ---------- */
.shell{position:relative;flex:1 1 auto;min-height:0;display:flex}

.content{
  flex:1 1 auto;min-width:0;
  padding:4px 12px 24px;
  display:flex;flex-direction:column;gap:14px;
}

.page-title{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:6px 4px 2px;flex-wrap:wrap;
}
.page-title h1{
  font-family:var(--font);
  font-size:var(--fs-title);font-weight:600;letter-spacing:-.2px;line-height:var(--lh-title);
}
.page-title .sub{display:block;font-size:var(--fs-caption);color:var(--text-3);font-weight:400;margin-top:2px}
.page-title .actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}

/* ---------- Confirm dialog ---------- */
/* One round icon carries the tone — danger when something is about to be
   removed, warn when it cannot be — so the dialog is readable before the text
   is. Used by every "are you sure" modal; do not restyle these per page. */
.confirm-hero{display:flex;align-items:flex-start;gap:12px;padding:2px 0 14px}
.confirm-hero .mark{
  flex:0 0 auto;display:grid;place-items:center;
  width:44px;height:44px;border-radius:50%;
  background:var(--danger-bg);color:var(--danger);
}
.confirm-hero.warn .mark{background:var(--warn-bg);color:var(--warn)}
.confirm-hero .mark .ico{width:22px;height:22px}
.confirm-hero h2{font-size:var(--fs-h2);line-height:var(--lh-h2);font-weight:600;color:var(--text)}
.confirm-hero p{margin-top:3px;font-size:var(--fs-body);line-height:1.55;color:var(--text-2)}

/* The record being acted on: label on the left, value on the right, so the
   reader can check they are deleting the row they meant to. */
.confirm-facts{border:1px solid var(--stroke-strong);border-radius:var(--r-card);overflow:hidden}
.confirm-facts .r{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:var(--fs-body)}
.confirm-facts .r + .r{border-top:1px solid var(--stroke)}
.confirm-facts .r .k{flex:0 0 40%;color:var(--text-2)}
.confirm-facts .r .v{flex:1 1 auto;text-align:right;font-weight:600;color:var(--text)}
/* A second line under a value — the house numbers behind "4 houses", say. */
.confirm-facts .r .sub{
  display:block;margin-top:1px;
  font-size:var(--fs-caption);line-height:var(--lh-caption);
  font-weight:400;color:var(--text-3);
}

/* Free text somebody typed, shown back as they typed it. Line breaks are part
   of what they wrote — collapsing them turns a short list into a run-on
   sentence. Looser leading than the ramp because Thai vowels and tone marks
   stack two levels and collide at a flat 20px over several lines. */
.pre-line{white-space:pre-line;line-height:1.6}

/* A badge that is also the way to change what it reports — the LINE status on
   a resident, which opens the dialog that undoes the link. It has to look
   pressable without turning into a button: the state is still the message, and
   a row of real buttons would read as a toolbar. */
a.badge.is-action{text-decoration:none;cursor:pointer;border-color:currentColor}
a.badge.is-action .ico,
a.badge.is-action .ico-sm{width:13px;height:13px;opacity:.75}
a.badge.is-action:hover{text-decoration:none;filter:brightness(.94)}
a.badge.is-action:hover .ico,
a.badge.is-action:hover .ico-sm{opacity:1}
a.badge.is-action:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* Two ways into the resident screens — LINE and a password — side by side.
   One panel each rather than a stack with a rule through it: they are
   alternatives, not steps, and reading them as a pair is the point. Each panel
   carries its own state and its own buttons, so the action is never marooned
   at the far edge of the card away from what it acts on.
   auto-fit rather than a breakpoint: they sit side by side when there is room
   for both and stack when there is not, which is the same answer a media query
   would give without pinning it to one width. */
.access-grid{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}

.access-panel{
  display:flex;flex-direction:column;
  padding:12px 14px;
  border:1px solid var(--stroke-strong);
  border-radius:var(--r-card);
  background:var(--surface-alt);
}
.access-panel-head{display:flex;align-items:center;gap:8px;margin-bottom:10px}
.access-panel-head .ico,.access-panel-head .ico-sm{color:var(--accent);flex:0 0 auto}
.access-panel-head b{font-size:var(--fs-body);font-weight:600;color:var(--text)}
/* The state belongs to the panel, so it sits at its far edge, not in the body. */
.access-panel-head .badge{margin-left:auto}

/* Grows, so both panels' buttons line up even when one holds more detail. */
.access-facts{flex:1 1 auto;display:flex;flex-direction:column;gap:8px}
.access-facts .k{
  display:block;
  font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-2);
}
.access-facts .v{display:block;font-size:var(--fs-body);color:var(--text);word-break:break-all}
.access-facts .hint{display:block;line-height:1.55}

.access-actions{
  display:flex;flex-wrap:wrap;gap:6px;
  margin-top:12px;padding-top:10px;
  border-top:1px solid var(--stroke);
}
.access-actions .btn{height:32px;padding:0 10px;font-size:var(--fs-btn);color:var(--text)}
.access-actions .btn .ico,.access-actions .btn .ico-sm{width:16px;height:16px;color:var(--accent)}
.access-actions .btn.is-danger{color:var(--danger)}
.access-actions .btn.is-danger .ico,.access-actions .btn.is-danger .ico-sm{color:var(--danger)}
.access-actions .inline-form{display:contents}

/* A username or an account id: read back over the phone character by
   character, so the digits have to be unmistakable. */
.mono{
  font-family:Consolas,"Cascadia Mono",Menlo,monospace;
  font-variant-numeric:tabular-nums;
  letter-spacing:.01em;
}

/* ---------- Sidebar / drawer ---------- */
.scrim{
  position:fixed;top:56px;left:0;right:0;bottom:0;z-index:8;
  background:transparent;
  opacity:0;pointer-events:none;
  transition:opacity var(--dur) var(--ease);
}
.app[data-drawer="open"] .scrim{opacity:1;pointer-events:auto}

.drawer-clip{
  position:fixed;z-index:9;
  top:56px;left:0;right:0;bottom:0;
  overflow:hidden;
  pointer-events:none;
}

.sidebar{
  position:absolute;
  top:4px;left:50%;
  height:80dvh;
  width:95%;max-width:none;
  background:var(--flyout);
  -webkit-backdrop-filter:blur(40px) saturate(160%);
  backdrop-filter:blur(40px) saturate(160%);
  border:1px solid var(--appbar-border);
  border-radius:14px;
  box-shadow:0 18px 40px rgba(20,40,70,.28), inset 0 1px 0 var(--appbar-highlight);
  transform:translate(-50%,calc(-100% - 90px));
  transition:transform var(--dur) var(--ease);
  pointer-events:auto;
  display:flex;flex-direction:column;
  padding:8px 6px 12px;
  overflow-y:auto;
  overscroll-behavior:contain;
}
.sidebar::-webkit-scrollbar{width:6px}
.sidebar::-webkit-scrollbar-thumb{background:var(--stroke-strong);border-radius:99px}
.sidebar > *{flex:0 0 auto}
.app[data-drawer="open"] .sidebar{transform:translate(-50%,0)}

/* The quick jump at the top of the sidebar.
   Shaped like the nav rows around it — same height, same padding, same icon
   gap — so it reads as part of the list rather than a control dropped on top
   of it. The field itself carries no border: the wrapper is what shows focus,
   the same way .input-affix works in forms. */
.nav-search{
  display:flex;align-items:center;gap:8px;
  margin:0 0 4px;padding:8px;
  border:1px solid var(--stroke);border-radius:var(--r-row);
  background:var(--card-solid);
  color:var(--text-3);
}
.nav-search:focus-within{
  border-color:var(--accent);
  box-shadow:0 0 0 1px var(--accent);
}
.nav-search .ico,.nav-search .ico-sm{flex:0 0 auto}
.nav-search input{
  flex:1 1 auto;min-width:0;
  border:0;background:transparent;outline:none;
  font:inherit;font-size:var(--fs-body);color:var(--text);
}
.nav-search input::placeholder{color:var(--text-3)}
/* The suggestions. Same glass surface as .menu, because it is a dropdown —
   it just holds houses instead of menu items. */
.nav-search-wrap{position:relative;z-index:30}
.nav-suggest{
  position:absolute;top:calc(100% + 4px);left:0;right:0;
  margin:0;padding:4px;list-style:none;
  background:var(--flyout);
  -webkit-backdrop-filter:blur(30px) saturate(160%);
  backdrop-filter:blur(30px) saturate(160%);
  border:1px solid var(--stroke);
  border-radius:9px;
  box-shadow:0 12px 32px rgba(20,40,70,.26);
  max-height:46dvh;overflow-y:auto;
}
.nav-suggest li{
  display:flex;align-items:baseline;gap:8px;
  padding:7px 10px;border-radius:6px;cursor:pointer;
  font-size:var(--fs-body);color:var(--text);
}
.nav-suggest li b{font-weight:600;flex:0 0 auto}
.nav-suggest li span{
  color:var(--text-3);font-size:var(--fs-caption);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.nav-suggest li:hover,
.nav-suggest li[aria-selected="true"]{background:var(--selected)}
/* Nothing matched. Not an option, so it takes no pointer and no highlight. */
.nav-suggest li.none{
  cursor:default;color:var(--text-3);font-size:var(--fs-caption);
}
.nav-suggest li.none:hover{background:transparent}
/* The app-bar mark is the way home now that the sidebar row is a search box. */
.breadcrumb .crumb-home{
  display:inline-flex;align-items:center;flex:0 0 auto;
  color:inherit;text-decoration:none;border-radius:6px;
}
.breadcrumb .crumb-home:hover{color:var(--accent)}
.breadcrumb .crumb-home:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

.sidebar .row{position:relative;padding:8px}
.sidebar .row[aria-current="page"]{background:var(--selected);font-weight:600}
.sidebar .row[aria-current="page"]::before{
  content:"";position:absolute;left:-6px;top:50%;transform:translateY(-50%);
  width:3px;height:16px;border-radius:2px;background:var(--accent);
}

.nav-group{
  font-size:var(--fs-caption);font-weight:600;letter-spacing:.5px;text-transform:uppercase;
  color:var(--text-3);padding:10px 10px 4px;
}
.divider{height:1px;background:var(--stroke-strong);margin:8px}

.sidebar-foot{
  margin-top:auto;padding:8px 8px 0;
  border-top:1px solid var(--stroke-strong);
  font-size:var(--fs-caption);color:var(--text-3);
}
.sidebar-foot .who{display:flex;flex-direction:column;gap:2px;padding:6px 2px}
.sidebar-foot .who b{font-size:var(--fs-body);color:var(--text);font-weight:600}

/* ============================================================
   4. COMPONENTS
   ============================================================ */
.card{
  background:var(--card);
  border:1px solid var(--stroke);
  border-radius:var(--r-card);
  box-shadow:var(--shadow-card);
  overflow:hidden;
}
.card-head{
  line-height:var(--lh-h2);
  display:flex;align-items:center;gap:8px;
  padding:9px 8px 9px 12px;
  font-size:var(--fs-h2);font-weight:600;color:var(--text);
}
.card-head .head-icon{color:var(--text-2);display:grid;place-items:center}
.card-head .spacer{flex:1}
/* Command bar in a card head — Outlook style: the heading, a hairline, then
   the actions for that card. The hint that follows .spacer stays caption-sized
   so it never competes with the heading it sits beside. */
.card-head .vsep{flex:0 0 auto;width:1px;height:18px;background:var(--stroke-strong)}
/* A head carrying more than a title and one control needs to fold on a phone
   rather than squeeze its own contents out of the card. */
.card-head.wrap{flex-wrap:wrap;row-gap:4px}
.card-head .hint{font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:400;color:var(--text-3)}
.card-head .btn{height:32px;padding:0 10px;font-size:var(--fs-btn);color:var(--text)}
.card-head .btn .ico,.card-head .btn .ico-sm{width:16px;height:16px;color:var(--accent)}
.card-body{padding:0 6px 6px}
.card-body.pad{padding:12px}
.card-foot{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  padding:10px 12px;border-top:1px solid var(--stroke);
}

/* ---------- Row ---------- */
.row{
  display:flex;align-items:center;gap:10px;
  width:100%;
  padding:7px 8px;
  border:0;background:transparent;
  border-radius:var(--r-row);
  font:inherit;font-size:var(--fs-body);color:var(--text);
  text-align:left;cursor:pointer;
  transition:background 140ms var(--ease);
}
.row:hover{background:var(--hover)}
.row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.row .label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.row .sub{display:block;font-size:var(--fs-caption);color:var(--text-3);margin-top:1px}
.row .chev{color:var(--text-3);flex:0 0 auto}
.row .pin{color:var(--text-3);opacity:.65;flex:0 0 auto}
a.row{text-decoration:none}
a.row:hover{text-decoration:none}

/* ---------- Icons ---------- */
.ico{width:16px;height:16px;flex:0 0 auto;display:block}
.ico-lg{width:17px;height:17px}
.ico-sm{width:13px;height:13px}

/* ---------- Stat tiles ---------- */
.stats{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
.stat{
  background:var(--card);
  border:1px solid var(--stroke);
  border-radius:var(--r-card);
  box-shadow:var(--shadow-card);
  padding:12px;
  display:flex;flex-direction:column;gap:4px;
}
.stat .k{display:flex;align-items:center;gap:6px;font-size:var(--fs-caption);color:var(--text-2)}
.stat .v{font-size:var(--fs-stat);line-height:var(--lh-stat);font-weight:600;letter-spacing:-.4px;font-variant-numeric:tabular-nums}
.stat .n{font-size:var(--fs-caption);color:var(--text-3)}
/* A figure that is also the way to the page behind it. Reads as the same tile
   until it is pointed at, because a row of tiles where some are blue links and
   some are not looks broken rather than informative. */
a.stat{color:inherit;text-decoration:none}
a.stat:hover{border-color:var(--accent);text-decoration:none}
a.stat:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* The number, or the line under it, carrying its own warning. */
.stat .v.is-warn{color:var(--warn)}
.stat .n.is-warn{color:var(--warn);font-weight:600}
/* A stat may carry the bar that explains its own number (how far along a vote
   is, and the line it has to cross) — see .progress. */
.stat .progress{margin:3px 0 1px}

/* ---------- Data table ---------- */
.tbl-wrap{padding:2px 6px 8px;overflow-x:auto}
.data-table{width:100%;border-collapse:collapse;font-size:var(--fs-body)}
.data-table th,.data-table td{text-align:left;padding:8px 6px;vertical-align:middle}
.data-table thead th{
  font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:600;color:var(--text-3);
  letter-spacing:.2px;border-bottom:1px solid var(--stroke-strong);white-space:nowrap;
}
.data-table tbody td{border-bottom:1px solid var(--stroke);color:var(--text-2)}
.data-table tbody tr:last-child td{border-bottom:0}
.data-table tbody tr{transition:background 140ms var(--ease)}
.data-table tbody tr:hover td{background:var(--hover)}
.data-table .c-name{color:var(--text);font-weight:600}
.data-table .c-name .nm{display:flex;align-items:center;gap:8px}
.data-table .c-name .nm .ico{flex:0 0 auto}
/* The way into a row.
   Whole rows are clickable (tr[data-href] in app.js), but a <tr> takes no
   focus and answers no key, so with a keyboard — or with JS off — a clickable
   row is not a way in at all. The name is therefore a real link, and it is the
   only one the table needs: a separate "view" icon beside it pointed at the
   same page and was removed.
   Styled as the text it already was, so nothing in the tables looks different
   until it is hovered or focused. Underline on hover says it can be clicked;
   the ring says where the keyboard is. */
.data-table .c-link{color:inherit;text-decoration:none}
.data-table .c-link:hover{color:inherit;text-decoration:underline}
.data-table .c-link:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;border-radius:4px;
}
.data-table .c-num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
/* A figure that is its own warning — a fund committed past its cash. The number
   still reads as a number; the tone only makes it findable in a long list. */
.data-table .c-num.is-warn{color:var(--warn);font-weight:600}
/* A sub-line under a cell's main text: the project's number, the entry a
   reversal undoes, how much of one came back. There is no global .hint rule,
   so without this these read as full-size body text run on to the end of the
   line above — which is exactly how they used to read. Caption size, per the
   Fluent ramp. */
.data-table .hint{
  display:block;
  margin-top:2px;
  font-size:var(--fs-caption);line-height:1.5;font-weight:400;color:var(--text-3);
}
/* "waiting 193 days" on a request nobody has voted on. Same caption size as any
   other sub-line — only the tone changes, because the row is still a normal
   pending row: time never decides anything here, it only asks to be noticed. */
.data-table .hint.is-warn{color:var(--warn);font-weight:600}
.data-table .hint a{color:inherit;text-decoration:underline dotted}
.data-table .hint a:hover{color:var(--accent);text-decoration:underline}
/* Jumping to the entry a reversal undoes has to land somewhere visible: the
   app bar is fixed, so leave room above it. */
.data-table tr:target > td{background:var(--selected)}
.data-table tr:target{scroll-margin-top:84px}
.data-table .c-act{text-align:right;white-space:nowrap;width:1%}
.data-table .c-act .iconbtn{display:inline-grid;vertical-align:middle}

/* ---------- Dropdown menu ---------- */
.menu-wrap{position:relative;flex:0 0 auto}
.menu{
  position:absolute;top:calc(100% + 6px);right:0;z-index:20;
  min-width:200px;
  background:var(--flyout);
  -webkit-backdrop-filter:blur(30px) saturate(160%);
  backdrop-filter:blur(30px) saturate(160%);
  border:1px solid var(--stroke);
  border-radius:9px;
  box-shadow:0 12px 32px rgba(20,40,70,.26);
  padding:6px;
  display:flex;flex-direction:column;gap:2px;
  max-height:70dvh;overflow-y:auto;
}
.menu-label{font-size:var(--fs-caption);color:var(--text-3);font-weight:600;letter-spacing:.3px;padding:6px 10px 3px}
.menu-item{
  display:flex;align-items:center;gap:8px;
  width:100%;
  border:0;background:transparent;color:var(--text);
  font:inherit;font-size:var(--fs-body);text-align:left;
  padding:8px 10px;border-radius:6px;cursor:pointer;
  text-decoration:none;
}
.menu-item:hover:not([disabled]){background:var(--hover);text-decoration:none}
.menu-item:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.menu-item[disabled]{opacity:.4;cursor:default}
.menu-item .check{width:14px;flex:0 0 auto;color:var(--accent);opacity:0;font-size:var(--fs-caption)}
.menu-item[aria-checked="true"] .check{opacity:1}
.menu-item.danger{color:var(--danger)}
.menu-sep{height:1px;background:var(--stroke-strong);margin:5px 6px}

/* ---------- Help popover ---------- */
/* A "?" next to a page title, for the explanation that would otherwise sit on
   the page as a permanent banner nobody reads twice. Same glass surface as
   .menu — it IS a dropdown, it just holds prose instead of items.
   Use it for text that explains a rule; keep .alert for something the reader
   has to act on or a state they need to see without asking. */
.help{position:relative;display:inline-flex;vertical-align:middle;margin-left:6px}
.help-btn{
  width:21px;height:21px;flex:0 0 auto;
  display:grid;place-items:center;
  border:1px solid var(--stroke-strong);border-radius:50%;
  background:transparent;color:var(--text-3);
  font:inherit;font-size:var(--fs-caption);font-weight:600;line-height:1;
  cursor:pointer;
  transition:background 140ms var(--ease),color 140ms var(--ease),border-color 140ms var(--ease);
}
.help-btn:hover{background:var(--hover);color:var(--text)}
.help-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.help-btn[aria-expanded="true"]{background:var(--selected);color:var(--accent);border-color:var(--accent)}
.help-pop{
  position:absolute;top:calc(100% + 8px);left:0;z-index:30;
  display:block;
  width:min(440px,calc(100vw - 32px));
  background:var(--flyout);
  -webkit-backdrop-filter:blur(30px) saturate(160%);
  backdrop-filter:blur(30px) saturate(160%);
  border:1px solid var(--stroke);
  border-radius:9px;
  box-shadow:0 12px 32px rgba(20,40,70,.26);
  padding:11px 13px;
  /* Reset the type: the button often sits inside an h1, and prose in here is
     multi-line Thai — 1.65 keeps the two rows of vowels and tone marks apart. */
  font-size:var(--fs-body);font-weight:400;line-height:1.65;
  letter-spacing:normal;color:var(--text);text-align:left;white-space:normal;
  /* .modal-card clips its overflow, so a long panel has to scroll inside
     itself rather than run past the bottom of the dialog. */
  max-height:min(60dvh,340px);overflow-y:auto;
}
/* In a dialog the button sits inside the <h2>, which is 16px — the panel keeps
   its own 14px body type either way (see the reset above). */
.modal-head .help-pop{width:min(420px,calc(100vw - 64px))}
.help-pop .p{display:block}
.help-pop .p + .p{margin-top:8px}
.help-pop b{font-weight:600}

/* The "!" twin of .help-btn: it reopens a notice about the reader's own
   situation ("you filed this, so you cannot vote on it") rather than explaining
   a rule. Built by app.js, which also hides the .alert the server rendered —
   with no JavaScript the banner stays and this button never exists. Tinted,
   because unlike "?" it says something is already true rather than offering to
   explain.

   It rides the sub-line under the page title, not the title itself: the heading
   already carries "?" and a second circle beside it reads as a pair of controls
   competing for the same job. */
.notice-btn{
  width:18px;height:18px;flex:0 0 auto;
  display:inline-grid;place-items:center;vertical-align:-3px;margin-right:6px;
  border:1px solid var(--warn);border-radius:50%;
  background:var(--warn-bg);color:var(--warn);
  font:inherit;font-size:var(--fs-caption);font-weight:600;line-height:1;
  cursor:pointer;
  transition:filter 140ms var(--ease);
}
/* Fallback place: a page whose title has no sub-line. */
.page-title h1 > .notice-btn{width:21px;height:21px;margin:0 0 0 6px;vertical-align:middle}
.notice-btn.info{border-color:var(--info);background:var(--info-bg);color:var(--info)}
.notice-btn:hover{filter:brightness(.95)}
.notice-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* Body of that dialog. One icon, one sentence — no form, so it gets room to
   breathe instead of the tight rhythm of .card-body. Side padding matches
   --modal-pad so the text lines up with the heading above and the button below.
   1.6 for the same reason .help-pop uses 1.65: stacked Thai vowels collide at
   the px ramp. */
.notice-body{
  display:flex;gap:12px;align-items:flex-start;
  padding:22px var(--modal-pad);
}
.notice-body .ico{flex:0 0 auto;width:24px;height:24px;color:var(--warn);margin-top:-1px}
.notice-body.info .ico{color:var(--info)}
.notice-body .notice-text{font-size:var(--fs-body);line-height:1.6;color:var(--text)}
/* 680px is sized for a form. One sentence in that much box reads as a mistake,
   so a notice gets a narrower card. */
.modal-card.is-notice{max-width:460px}

/* ============================================================
   5. FORMS & BUTTONS
   ============================================================ */
.form-grid{display:grid;grid-template-columns:1fr;gap:12px}
.form-grid.cols-2{grid-template-columns:1fr}

.field{display:flex;flex-direction:column;gap:5px;min-width:0}
.field > label{font-size:var(--fs-body);font-weight:400;color:var(--text)}
.field .req{color:var(--danger);margin-left:2px}
.field .hint{font-size:var(--fs-caption);color:var(--text-3)}
.field .err{font-size:var(--fs-caption);color:var(--danger)}
.field.span-2{grid-column:1 / -1}

/* A field that SHOWS a stored value instead of taking one — the detail cards
   on /projects/{id}, /disbursements/{id}, /payments/{id} and the confirm
   dialogs.
   In a form the input box is what separates the label from the value. With no
   box, label and value are the same size, weight and colour, and eleven of
   them in a card read as one grey block with no way to tell a heading from
   data. So the label drops to caption grey and the value keeps body size —
   the pair reads top-down instead of as two equal lines.
   Form labels are untouched and stay 14 / 400 / --text: CLAUDE.md fixes that
   for Outlook-style forms, and this is a different component. */
.field.ro{gap:2px}
.field.ro > label{
  font-size:var(--fs-caption);line-height:var(--lh-caption);
  color:var(--text-3);
}
/* Values are read, not scanned for edges, so they get the looser leading that
   multi-line Thai needs — vowels and tone marks stack two deep and collide at
   a flat 20px. Replaces the inline line-height these cards used to carry. */
.field.ro > div{line-height:1.55}

.input,.select,.textarea{
  width:100%;
  height:var(--field-h);padding:0 13px;
  font:inherit;font-size:var(--fs-body);
  color:var(--text);
  background:var(--surface);
  border:1px solid var(--stroke-strong);
  border-bottom-width:2px;
  border-radius:var(--r-field);
  transition:border-color 140ms var(--ease), background 140ms var(--ease);
}
.input:hover,.select:hover,.textarea:hover{background:var(--card-solid)}
/* Example text inside an empty box. It has to read as a suggestion, not as a
   value already typed — browsers pick their own grey, which lands too close to
   --text in dark mode, so say it with the token used for hints. */
.input::placeholder,.textarea::placeholder{color:var(--text-3);opacity:1}
.input:focus,.select:focus,.textarea:focus{
  outline:none;border-color:var(--accent);
  box-shadow:0 0 0 3px var(--focus-ring);
}
.input[aria-invalid="true"],.select[aria-invalid="true"],.textarea[aria-invalid="true"]{
  border-color:var(--danger);
}
.input[readonly],.input[disabled],.select[disabled]{opacity:.6;cursor:not-allowed}

/* Date and month fields.
   Two things are wrong with these out of the box. Chrome's calendar button is
   a thin near-black glyph that disappears against the field, and clicking the
   TEXT of the field does not open the calendar at all — it drops a caret into
   the day or month segment, so people read "วว/ดด/ปปปป" as an empty box and
   type the date by hand. Typing is where the mistakes come from.

   app.js makes the whole field open the picker (showPicker); these rules make
   it look that way — a pointer everywhere, and a button that reads as a button.
   Firefox draws no such button and ignores the pseudo-element; the field keeps
   working there either way. */
.input[type="date"],.input[type="month"],.input[type="datetime-local"]{cursor:pointer}
.input[type="date"]::-webkit-calendar-picker-indicator,
.input[type="month"]::-webkit-calendar-picker-indicator,
.input[type="datetime-local"]::-webkit-calendar-picker-indicator{
  cursor:pointer;
  width:20px;height:20px;padding:5px;margin-right:-5px;
  border-radius:var(--r-row);
  background-color:var(--selected);
  transition:box-shadow 120ms var(--ease);
}
/* Hover marks the button with a ring rather than a darker fill: Chrome draws
   the calendar glyph itself in a fixed dark colour that does not inherit from
   us, so filling with --accent would put dark on blue and lose the glyph. */
.input[type="date"]:hover::-webkit-calendar-picker-indicator,
.input[type="month"]:hover::-webkit-calendar-picker-indicator,
.input[type="datetime-local"]:hover::-webkit-calendar-picker-indicator,
.input[type="date"]:focus::-webkit-calendar-picker-indicator,
.input[type="month"]:focus::-webkit-calendar-picker-indicator,
.input[type="datetime-local"]:focus::-webkit-calendar-picker-indicator{
  box-shadow:inset 0 0 0 1px var(--accent);
}
.textarea{height:auto;min-height:84px;padding:10px 13px;resize:vertical}

/* Field with a button parked at its trailing edge (e.g. "re-roll code").
   The WRAPPER owns the border and the focus ring; the input inside goes
   borderless. Laid out with flex — not absolute positioning — so the button
   can never drift out of the field, and the field still looks right if this
   stylesheet loads late. */
.input-affix{
  display:flex;align-items:center;gap:4px;
  width:100%;height:var(--field-h);
  padding-right:4px;
  background:var(--surface);
  border:1px solid var(--stroke-strong);
  border-radius:var(--r-field);
  transition:border-color 140ms var(--ease), box-shadow 140ms var(--ease);
}
.input-affix:hover{background:var(--card-solid)}
.input-affix:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus-ring)}
.input-affix:has(.input[aria-invalid="true"]){border-color:var(--danger)}

.input-affix .input{
  flex:1 1 auto;min-width:0;
  height:100%;
  border:0;background:transparent;border-radius:inherit;
}
.input-affix .input:hover{background:transparent}
.input-affix .input:focus{box-shadow:none}
/* Generated data, not a disabled control — keep it fully readable. */
.input-affix .input[readonly]{opacity:1;cursor:default;font-variant-numeric:tabular-nums;letter-spacing:.3px}

.input-affix .affix-btn{
  flex:0 0 auto;
  display:inline-flex;align-items:center;justify-content:center;
  height:30px;padding:0 12px;
  font:inherit;font-size:var(--fs-btn-sm);font-weight:600;line-height:1;
  /* Accent-tinted so it reads as an action, not as part of the value */
  color:var(--accent);
  background:var(--selected);
  border:1px solid transparent;
  border-radius:var(--r-row);
  cursor:pointer;white-space:nowrap;
  transition:background 140ms var(--ease), color 140ms var(--ease), border-color 140ms var(--ease);
}
.input-affix .affix-btn:hover{background:var(--accent);border-color:var(--accent);color:var(--accent-contrast)}
.input-affix .affix-btn:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.input-affix .affix-btn[disabled]{opacity:.45;pointer-events:none}

/* ---------- File field ----------
   Left alone, this is the one control on every form that belongs to the
   browser rather than to us: a grey OS button and the words "ไม่ได้เลือกไฟล์ใด"
   in whatever size the browser feels like. Next to a row of 40px fields with
   our own border and radius, it reads as something that fell out of a
   different program (ผู้พัฒนาแจ้ง 2026-09-17).

   The frame is the same .input frame as every other control; only the inside
   changes. The button reuses the .affix-btn idea — accent-tinted, so it reads
   as an action rather than as part of the value — and the file name sits
   beside it in the field, exactly where a typed value would be.

   ::file-selector-button is the standard; ::-webkit-file-upload-button is the
   same thing under the older WebKit name and has to be its own rule, because
   a selector list containing one pseudo-element a browser does not know drops
   the WHOLE rule. */
.input[type="file"]{
  /* The button supplies the left inset, so the field's own padding would
     double it. Height stays --field-h: this lines up with its neighbours. */
  padding:0 13px 0 6px;
  /* The browser's "no file chosen" is not a value, it is the absence of one —
     grey like a placeholder until JS marks the field as holding a file. */
  color:var(--text-3);
  cursor:pointer;
  /* Centres the text and the button inside the 40px box in Chrome/Edge,
     which lay the contents out on a single line box. */
  line-height:calc(var(--field-h) - 4px);
  /* A long file name must not push the button out of the field. */
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.input[type="file"][data-has-file]{color:var(--text)}

.input[type="file"]::file-selector-button{
  margin-right:12px;
  height:28px;padding:0 13px;
  font:inherit;font-size:var(--fs-btn-sm);font-weight:600;line-height:1;
  color:var(--accent);
  background:var(--selected);
  border:1px solid transparent;
  border-radius:var(--r-row);
  cursor:pointer;
  transition:background 140ms var(--ease), color 140ms var(--ease), border-color 140ms var(--ease);
}
.input[type="file"]:hover::file-selector-button{
  background:var(--accent);border-color:var(--accent);color:var(--accent-contrast);
}
.input[type="file"]::-webkit-file-upload-button{
  margin-right:12px;
  height:28px;padding:0 13px;
  font:inherit;font-size:var(--fs-btn-sm);font-weight:600;line-height:1;
  color:var(--accent);
  background:var(--selected);
  border:1px solid transparent;
  border-radius:var(--r-row);
  cursor:pointer;
  transition:background 140ms var(--ease), color 140ms var(--ease), border-color 140ms var(--ease);
}
.input[type="file"]:hover::-webkit-file-upload-button{
  background:var(--accent);border-color:var(--accent);color:var(--accent-contrast);
}

/* What was picked, under the field: the thumbnail answers "is this the right
   photo" before anything is uploaded, which is the question people actually
   have — the slip of the wrong month and the photo of the wrong wall both
   look fine as a file name. Built by app.js, so a browser without JS simply
   keeps the file name in the field and loses nothing it needs. */
.file-preview{
  display:flex;align-items:center;gap:10px;
  margin-top:8px;padding:8px;
  background:var(--hover);
  border:1px solid var(--stroke);
  border-radius:var(--r-row);
}
.file-preview img{
  width:56px;height:56px;flex:0 0 auto;
  object-fit:cover;
  border-radius:var(--r-row);
  border:1px solid var(--stroke);
  background:var(--card-solid);
}
.file-preview .nm{
  min-width:0;
  font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.file-preview .sz{
  margin-left:auto;flex:0 0 auto;
  font-size:var(--fs-caption);color:var(--text-3);
  font-variant-numeric:tabular-nums;
}
/* Take it back off. Grey rather than .iconbtn.danger: nothing has been
   uploaded or stored yet, so this undoes a choice rather than destroying
   anything — the same reading as the × that closes a dialog. It is the last
   thing in the row because it is the one control here that changes the form. */
.file-preview .rm{width:26px;height:26px;margin-left:2px}
.file-preview .rm:hover{background:var(--danger-bg);color:var(--danger)}
.select{appearance:none;background-image:none;cursor:pointer}

.check-line{display:flex;align-items:center;gap:8px;font-size:var(--fs-body);color:var(--text)}
.check-line input[type="checkbox"]{width:16px;height:16px;accent-color:var(--accent);cursor:pointer}

.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
  height:var(--field-h);padding:0 15px;
  font:inherit;font-size:var(--fs-btn);font-weight:600;
  color:var(--text);
  background:var(--surface);
  border:1px solid var(--stroke-strong);
  border-radius:var(--r-field);
  cursor:pointer;text-decoration:none;
  transition:background 140ms var(--ease), border-color 140ms var(--ease), opacity 140ms var(--ease);
}
.btn:hover{background:var(--hover);text-decoration:none}
.btn:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.btn[disabled]{opacity:.45;pointer-events:none}

.btn-primary{background:var(--accent);border-color:var(--accent);color:var(--accent-contrast)}
.btn-primary:hover{background:var(--accent-soft);border-color:var(--accent-soft)}

.btn-danger{background:transparent;border-color:var(--danger);color:var(--danger)}
.btn-danger:hover{background:var(--danger-bg)}

.btn-ghost{background:transparent;border-color:transparent;color:var(--text-2);font-weight:400}
.btn-ghost:hover{background:var(--hover)}

.btn-sm{height:32px;padding:0 11px;font-size:var(--fs-btn-sm)}

.form-actions{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  padding-top:4px;
}
/* Submit and cancel are the same size, so neither reads as the bigger choice.
   Cancel is a plain .btn — white with the standard grey border.
   .card-foot is where every real form puts its buttons; .form-actions is the
   auth screens. Both get the same treatment. */
.form-actions .btn,
.card-foot .btn{min-width:104px}
.form-actions .spacer{flex:1}

/* Filter bar above a list */
.filters{
  display:flex;align-items:flex-end;gap:8px;flex-wrap:wrap;
  padding:10px 12px;
}
.filters .field{flex:1 1 160px}
.filters .field.narrow{flex:0 1 140px}

/* ============================================================
   5b. MODAL  (see CLAUDE.md → "Modal")
   Every command button opens one of these instead of navigating. The scrim
   blocks the page behind it until the user cancels or saves.
   The dialog shows the SAME view file the full page uses, so .card /
   .card-body / .card-foot coming from that view are restyled here rather
   than duplicated in every form.
   ============================================================ */
.modal{
  position:fixed;inset:0;z-index:60;
  display:none;
  align-items:center;justify-content:center;
  padding:24px;
}
.modal[data-open]{display:flex}

/* Its own scrim — the shell's .scrim belongs to the drawer. */
.modal-scrim{
  position:absolute;inset:0;
  background:rgba(12,20,32,.44);
  -webkit-backdrop-filter:blur(2px);
  backdrop-filter:blur(2px);
}
[data-theme="dark"] .modal-scrim{background:rgba(0,0,0,.6)}

/* Locks the page behind the dialog. */
body.modal-open{overflow:hidden}

.modal-card{
  /* One knob for the gutter on every edge. A dialog gets more breathing
     room than an inline card — content must not run into the edges. */
  --modal-pad:24px;

  position:relative;
  display:flex;flex-direction:column;min-height:0;
  width:100%;max-width:680px;
  max-height:calc(100dvh - 48px);
  background:var(--card);
  border:1px solid var(--stroke);
  border-radius:var(--r-card);
  box-shadow:0 24px 60px rgba(12,20,32,.28);
  overflow:hidden;
}

.modal-head{
  flex:0 0 auto;
  display:flex;align-items:center;gap:8px;
  padding:18px var(--modal-pad);
  border-bottom:1px solid var(--stroke);
}
/* The close button keeps its hit area but sits flush with the padding edge. */
.modal-head .iconbtn{margin-right:-6px}
.modal-head h2{
  flex:1 1 auto;min-width:0;
  font-size:var(--fs-h2);line-height:var(--lh-h2);font-weight:600;color:var(--text);
}

/* ---- the fetched view's own chrome, adapted to the dialog ---- */
.modal-card .page-title{display:none}          /* the dialog has its own heading */
.modal-card > .card,
.modal-card > form,
.modal-card > section{
  flex:1 1 auto;min-height:0;
  display:flex;flex-direction:column;
}
.modal-card .card{
  border:0;border-radius:0;background:transparent;box-shadow:none;overflow:visible;
}
.modal-card form{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
/* The view's own padding is tuned for an inline card — override it here so
   the dialog keeps one consistent gutter on every edge. */
.modal-card .card-body,
.modal-card .card-body.pad{
  flex:1 1 auto;min-height:0;overflow-y:auto;
  padding:var(--modal-pad);
}
.modal-card .card-foot{
  flex:0 0 auto;background:var(--card);
  padding:16px var(--modal-pad);
  /* Dialog buttons sit at the trailing edge, primary first — Fluent order. */
  justify-content:flex-end;
}
.modal-card .alert{margin:16px var(--modal-pad) 0}
.modal-card .form-grid{gap:18px}
.modal-card .form-grid.cols-2{grid-template-columns:1fr 1fr}

.modal-loading{
  display:flex;align-items:center;justify-content:center;
  padding:40px 16px;color:var(--text-3);
}

/* Mobile: a sheet pinned to the bottom edge. */
@media (max-width:859.98px){
  .modal{padding:0;align-items:flex-end}
  .modal-card{
    --modal-pad:18px;
    max-width:none;max-height:92dvh;
    border-radius:14px 14px 0 0;
    border-left:0;border-right:0;border-bottom:0;
  }
  .modal-card .form-grid.cols-2{grid-template-columns:1fr}
}

/* ============================================================
   6. FEEDBACK
   ============================================================ */
.alert{
  display:flex;align-items:flex-start;gap:9px;
  padding:10px 12px;
  font-size:var(--fs-body);line-height:1.55;
  border:1px solid var(--stroke-strong);
  border-radius:var(--r-card);
  background:var(--info-bg);color:var(--text);
}
.alert .ico{margin-top:1px}
.alert.success{background:var(--ok-bg);border-color:var(--ok);color:var(--text)}
.alert.success .ico{color:var(--ok)}
.alert.error{background:var(--danger-bg);border-color:var(--danger)}
.alert.error .ico{color:var(--danger)}
.alert.warning{background:var(--warn-bg);border-color:var(--warn)}
.alert.warning .ico{color:var(--warn)}
.alert.info{background:var(--info-bg);border-color:var(--info)}
.alert.info .ico{color:var(--info)}
.alert .close{margin-left:auto}

/* ---------- Toast ---------- */
/* Flash messages float over the page as glass panels centred at the top edge,
   the same material as the app bar and the menus. They never take space in the
   flow, so a "saved" notice cannot push the row it refers to out from under
   the pointer. Each one empties a hairline over 3 seconds and then leaves;
   app.js listens for that animation ending rather than keeping its own timer,
   which is what makes pause-on-hover work for the dismissal too. */
.toast-wrap{
  position:fixed;z-index:60;
  top:12px;left:50%;transform:translateX(-50%);
  display:flex;flex-direction:column;align-items:center;gap:8px;
  width:min(420px, calc(100vw - 28px));
  pointer-events:none;                     /* the page stays usable around them */
}
.toast{
  position:relative;overflow:hidden;
  display:flex;align-items:center;gap:10px;
  padding:11px 12px 13px;
  font-size:var(--fs-body);line-height:1.55;color:var(--text);
  background:var(--flyout);
  -webkit-backdrop-filter:blur(40px) saturate(160%);
  backdrop-filter:blur(40px) saturate(160%);
  border:1px solid var(--appbar-border);
  border-radius:var(--r-card);
  box-shadow:0 12px 32px rgba(20,40,70,.22), inset 0 1px 0 var(--appbar-highlight);
  pointer-events:auto;
  animation:toast-in var(--dur) var(--ease) both;
}
.toast.leaving{animation:toast-out var(--dur) var(--ease) forwards}
/* The tone is carried by a filled round badge rather than a bare glyph — at a
   glance across the screen the colour is what says saved / blocked / wrong. */
.toast .mark{
  flex:0 0 auto;display:grid;place-items:center;
  width:34px;height:34px;border-radius:50%;
  background:var(--info-bg);color:var(--info);
}
.toast .mark .ico{width:20px;height:20px;margin:0}
.toast.success .mark{background:var(--ok-bg);color:var(--ok)}
.toast.error   .mark{background:var(--danger-bg);color:var(--danger)}
.toast.warning .mark{background:var(--warn-bg);color:var(--warn)}
.toast .close{margin-left:auto;flex:0 0 auto;align-self:flex-start}

/* The countdown line. Scaled rather than resized so it animates on the
   compositor; hover / keyboard focus pauses it. */
.toast .bar{position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--stroke)}
.toast .bar > i{
  display:block;height:100%;width:100%;
  transform-origin:left center;background:var(--accent);
  animation:toast-countdown 3s linear forwards;
}
.toast.success .bar > i{background:var(--ok)}
.toast.error   .bar > i{background:var(--danger)}
.toast.warning .bar > i{background:var(--warn)}
.toast:hover .bar > i,
.toast:focus-within .bar > i{animation-play-state:paused}

@keyframes toast-countdown{from{transform:scaleX(1)} to{transform:scaleX(0)}}
@keyframes toast-in{from{opacity:0;transform:translateY(-8px)} to{opacity:1;transform:none}}
@keyframes toast-out{to{opacity:0;transform:translateY(-6px)}}

/* Below the breakpoint they widen to the screen, still centred. */
@media (max-width:859.98px){
  .toast-wrap{top:10px;width:calc(100vw - 20px)}
}

/* Progress bar — how far a vote has got towards its threshold.
   The filled part is an <i> so the track keeps its own rounded corners. */
.progress{
  position:relative;
  height:8px;
  border-radius:999px;
  background:var(--hover);
  overflow:hidden;
}
.progress > i{
  display:block;
  height:100%;
  border-radius:999px;
  background:var(--accent);
  transition:width var(--dur) var(--ease);
}
.progress.ok > i{background:var(--ok)}
.progress.danger > i{background:var(--danger)}
/* The threshold that has to be reached, drawn across the track. */
.progress > b{
  position:absolute;
  top:-2px;
  bottom:-2px;
  width:2px;
  background:var(--text-2);
  opacity:.55;
}
@media (prefers-reduced-motion: reduce){
  .progress > i{transition:none}
  /* No countdown to watch, so app.js falls back to a plain timer instead. */
  .toast{animation:none}
  .toast .bar > i{animation:none}
}

/* Tally bar — where a vote stands, one cell per voter (partials/tally.php).
   Replaces .progress wherever real votes are being counted: with a committee
   of two, a percentage track is unreadable and disagrees with the rule
   (required_votes rounds up, so 50% needs 1 of 2 — which is 50% of the bar
   only by coincidence). Cells are counted, not measured.

   Three states, because a vote against is spent: it can never turn into
   support, and seeing that is what tells the reader whether the line is still
   reachable at all. */
/* Tight gaps and a small floor on the cells: this lives inside a .stat card,
   which is far narrower than the form .quorum sits in — 20 cells still fit
   without the row ever pushing the card wider. */
.tally{display:flex;gap:3px;height:10px;align-items:stretch;min-width:0}
.tally > i{
  flex:1 1 0;
  min-width:5px;
  border-radius:3px;
  background:var(--hover);
  border:1px solid var(--stroke-strong);
  transition:background var(--dur) var(--ease);
}
.tally > i.on{background:var(--accent);border-color:var(--accent)}
.tally > i.no{background:var(--danger-bg);border-color:var(--danger)}
.tally.ok > i.on{background:var(--ok);border-color:var(--ok)}
.tally.danger > i.on{background:var(--danger);border-color:var(--danger)}
/* The line to cross, drawn in the gap after the last cell that must be filled.
   In the gap, not on a cell: it marks a boundary between votes, not a vote. */
.tally > i.mark{position:relative}
.tally > i.mark::after{
  content:"";
  position:absolute;top:-3px;bottom:-3px;right:-3px;
  width:2px;border-radius:1px;
  background:var(--text-2);opacity:.6;
}
/* A referendum's electorate is the whole village — too many to count, so the
   same figures fall back to one track with the threshold marked on it. */
.tally.solid{
  display:block;position:relative;height:8px;
  border-radius:999px;background:var(--hover);overflow:visible;
}
.tally.solid > i{
  display:block;height:100%;border:0;border-radius:999px;
  background:var(--accent);
}
.tally.solid.ok > i{background:var(--ok)}
.tally.solid.danger > i{background:var(--danger)}
.tally.solid > b{
  position:absolute;top:-2px;bottom:-2px;
  width:2px;background:var(--text-2);opacity:.55;
}
@media (prefers-reduced-motion: reduce){
  .tally > i{transition:none}
}


/* Quorum strip — what a request is going to need, shown while it is still
   being typed. It is NOT a tally: nothing has been voted on yet, so reusing
   .progress here would read as "somebody already approved this much". Each
   cell is one committee member who could vote, and the filled cells are how
   many of them have to agree — countable by eye, which is the whole point
   when the committee is small and a percentage would lie (one member has to
   carry 100% of the bar under a 50% rule). */
.quorum{
  margin-top:16px;
  padding-top:12px;
  border-top:1px solid var(--stroke);
  display:flex;
  flex-direction:column;
  gap:7px;
}
.quorum .q-need{
  display:flex;align-items:flex-start;gap:8px;
  font-size:var(--fs-body);line-height:1.55;color:var(--text);
}
.quorum .q-need > .ico{flex:0 0 auto;margin-top:2px;color:var(--accent)}
.quorum .q-need b{font-weight:600}
.quorum .q-bar{display:flex;gap:4px;height:8px}
.quorum .q-bar > i{
  flex:1 1 0;
  min-width:6px;
  height:100%;
  border-radius:999px;
  background:var(--hover);
}
.quorum .q-bar > i.on{background:var(--accent)}
/* Too many members to count a cell at a time — one track filled to the
   share that has to agree. Same colours, so it still reads as the same thing. */
.quorum .q-bar.solid{
  display:block;
  border-radius:999px;
  background:var(--hover);
  overflow:hidden;
}
.quorum .q-bar.solid > i{display:block;flex:none;height:100%}
.quorum .q-note{
  font-size:var(--fs-caption);line-height:1.6;color:var(--text-3);
}

/* What a batch run would produce, under the preview table. The one figure that
   catches a mistyped rate before the bills exist, so it is body size and bold
   rather than another caption line. */
.batch-sum{
  display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  margin-top:10px;padding:10px 12px;
  border:1px solid var(--stroke-strong);border-radius:var(--r-row);
  background:var(--hover);
  font-size:var(--fs-body);line-height:1.55;font-weight:600;color:var(--text);
}
.batch-sum .hint{font-size:var(--fs-caption);font-weight:400;color:var(--text-3)}
.badge{
  display:inline-flex;align-items:center;gap:4px;
  padding:2px 8px;
  font-size:var(--fs-caption);font-weight:600;
  border-radius:99px;
  border:1px solid transparent;
  white-space:nowrap;
}
.badge.ok{background:var(--ok-bg);color:var(--ok)}
.badge.warn{background:var(--warn-bg);color:var(--warn)}
.badge.danger{background:var(--danger-bg);color:var(--danger)}
.badge.info{background:var(--info-bg);color:var(--info)}
.badge.due{background:var(--due-bg);color:var(--due)}
.badge.muted{background:var(--hover);color:var(--text-3)}
/* A badge may be the link to whatever it is counting. */
a.badge{text-decoration:none;transition:filter 140ms var(--ease)}
a.badge:hover{filter:brightness(.94)}
a.badge:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Two facts about one row — its status and its queue — wrap instead of
   widening the column. */
.badge-group{display:flex;flex-wrap:wrap;gap:4px;align-items:center}

/* A short code meant to be read out loud or copied — a LINE link code today.
   Digits are tabular and slightly spread so 0/8 and 1/7 cannot be misread over
   the phone. .spent greys out a code that can no longer be used. */
.code-chip{
  display:inline-flex;align-items:center;gap:6px;
  padding:2px 8px;
  border:1px solid var(--stroke-strong);border-radius:var(--r-row);
  background:var(--hover);
  font-variant-numeric:tabular-nums;letter-spacing:1.2px;
  font-size:var(--fs-body);line-height:var(--lh-body);
  white-space:nowrap;
}
.code-chip.spent{color:var(--text-3);text-decoration:line-through;letter-spacing:.6px}
.code-chip .iconbtn{width:24px;height:24px;letter-spacing:normal}

/* The freshly issued link code, sized to be read out over the phone or copied
   in one go. Digits are spaced in threes by the view, so the letter-spacing
   here only has to keep the groups apart. */
.code-display{
  display:flex;align-items:center;justify-content:center;gap:10px;
  padding:14px 12px;
  border:1px solid var(--stroke-strong);border-radius:var(--r-card);
  background:var(--hover);
}
.code-display .digits{
  font-size:var(--fs-stat);line-height:var(--lh-stat);font-weight:600;
  font-variant-numeric:tabular-nums;letter-spacing:2px;
  color:var(--text);white-space:nowrap;
}
.code-display .iconbtn{width:34px;height:34px;color:var(--accent)}
.code-display .iconbtn .ico{width:18px;height:18px}
.code-display [data-copy-mark]{font-size:var(--fs-caption);color:var(--ok);font-weight:600}
.code-display + .confirm-facts{margin-top:12px}

.empty{
  display:flex;flex-direction:column;align-items:center;gap:8px;
  padding:34px 16px;text-align:center;color:var(--text-3);
}
.empty .ico{width:30px;height:30px;opacity:.5}
.empty .t{font-size:var(--fs-h2);line-height:var(--lh-h2);font-weight:600;color:var(--text-2)}
.empty .d{font-size:var(--fs-body);max-width:340px;line-height:1.6}

.pagination{
  display:flex;align-items:center;gap:4px;flex-wrap:wrap;
  padding:10px 12px;border-top:1px solid var(--stroke);
  font-size:var(--fs-caption);color:var(--text-3);
}
.pagination .pages{display:flex;align-items:center;gap:3px;margin-left:auto;flex-wrap:wrap}
.pagination .pg{
  min-width:28px;height:28px;padding:0 7px;
  display:inline-grid;place-items:center;
  border:1px solid var(--stroke-strong);border-radius:var(--r-row);
  color:var(--text);background:transparent;font-size:var(--fs-body);text-decoration:none;
}
.pagination .pg:hover{background:var(--hover);text-decoration:none}
.pagination .pg[aria-current="page"]{background:var(--selected);border-color:var(--accent);color:var(--accent);font-weight:600}
.pagination .gap{padding:0 4px;color:var(--text-3)}

.inline-form{display:inline}

/* ============================================================
   7. AUTH SCREEN
   ============================================================ */
.auth-wrap{
  min-height:100dvh;
  display:grid;place-items:center;
  padding:24px 16px;
  background:
    radial-gradient(900px 500px at 15% -10%, var(--selected), transparent 60%),
    radial-gradient(700px 480px at 110% 20%, var(--info-bg), transparent 60%),
    var(--surface-alt);
}

/* Two panels side by side on desktop, form only on a phone. */
.auth-card{
  width:100%;max-width:420px;
  background:var(--card-solid, var(--card));
  border:1px solid var(--stroke);
  border-radius:16px;
  box-shadow:0 18px 50px rgba(20,40,70,.18), inset 0 1px 0 var(--appbar-highlight);
  overflow:hidden;
  display:grid;
  grid-template-columns:1fr;
}

/* --- welcome panel (desktop only) --- */
.auth-aside{display:none}
.auth-aside-head .hi{
  display:block;
  font-size:var(--fs-h2);line-height:var(--lh-h2);color:var(--text-2);
}
.auth-aside-head h1{
  font-family:var(--font);
  font-size:var(--fs-stat);line-height:var(--lh-stat);font-weight:600;letter-spacing:-.3px;
  margin-top:2px;
}
.auth-aside-head p{
  margin-top:8px;
  font-size:var(--fs-body);line-height:1.6;color:var(--text-2);
  max-width:30ch;
}
.auth-art{width:100%;height:auto;margin-top:auto;display:block}
.auth-art .ground{fill:var(--stroke)}
.auth-art .far{fill:var(--hover);stroke:var(--stroke);stroke-width:1.5;stroke-linejoin:round}
.auth-art .near{fill:var(--card-solid, var(--card));stroke:var(--accent);stroke-width:2;stroke-linejoin:round}
.auth-art .near .door{fill:var(--accent);stroke:none}
.auth-art .near .win{fill:var(--selected);stroke:var(--accent);stroke-width:1.5}
.auth-art .fence{fill:none;stroke:var(--text-3);stroke-width:2;stroke-linecap:round;opacity:.6}
.auth-art .tree{fill:var(--ok-bg);stroke:var(--ok);stroke-width:1.8;stroke-linecap:round}
.auth-art .sun{fill:var(--warn-bg);stroke:var(--warn);stroke-width:2}

/* --- form panel --- */
.auth-main{
  padding:22px 20px 18px;
  display:flex;flex-direction:column;gap:14px;
  min-width:0;
}
.auth-brand{display:flex;align-items:center;gap:10px}
.auth-brand .mark{
  width:36px;height:36px;flex:0 0 auto;
  display:grid;place-items:center;border-radius:10px;
  background:var(--accent);color:var(--accent-contrast);
}
.auth-brand h2{
  font-family:var(--font);
  font-size:var(--fs-title);line-height:var(--lh-title);font-weight:600;letter-spacing:-.2px;
}
.auth-brand .sub{display:block;font-size:var(--fs-caption);color:var(--text-3);font-weight:400}
.auth-lead{font-size:var(--fs-body);line-height:1.6;color:var(--text-2)}
.auth-form{display:flex;flex-direction:column;gap:14px}
.auth-submit{width:100%;justify-content:center;height:var(--field-h)}
.auth-hint{font-size:var(--fs-caption);color:var(--text-3);line-height:1.6}
.auth-foot{
  display:flex;align-items:center;justify-content:space-between;gap:8px;
  font-size:var(--fs-caption);color:var(--text-3);
  margin-top:auto;padding-top:10px;border-top:1px solid var(--stroke);
}

/* Input with a leading icon, and an optional reveal button at the end.
   The wrapper carries no border of its own — the input keeps it, so the focus
   ring stays exactly where the browser puts it. */
.input-icon{position:relative;display:block}
.input-icon > .ico{
  position:absolute;left:11px;top:50%;transform:translateY(-50%);
  color:var(--text-3);pointer-events:none;
}
.input-icon > .input{padding-left:36px;width:100%}
.input-icon > .input[type="password"],
.input-icon > .input[data-revealable]{padding-right:40px}
.input-icon > .reveal{
  position:absolute;right:4px;top:50%;transform:translateY(-50%);
  width:32px;height:32px;
  display:grid;place-items:center;
  border:0;background:transparent;border-radius:var(--r-row);
  color:var(--text-3);cursor:pointer;
}
.input-icon > .reveal:hover{background:var(--hover);color:var(--text)}
.input-icon > .reveal:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.input-icon > .reveal .ic-hide{display:none}
.input-icon > .reveal[aria-pressed="true"] .ic-show{display:none}
.input-icon > .reveal[aria-pressed="true"] .ic-hide{display:block}

/* ============================================================
   7b. RESIDENT SIDE (LIFF)
   ============================================================ */
/* Opened inside LINE, so a phone is not the small case here — it is the only
   case. Nothing below invents a colour or a type step: the tokens and the
   components are the same ones the admin side uses, arranged for a thumb. */

/* The bottom bar takes the place of the sidebar. Its height plus the phone's
   home indicator is padded onto the content so the last card is never trapped
   under it. */
.liff .content{padding-bottom:calc(70px + env(safe-area-inset-bottom, 0px))}
.liff.is-bare .content{padding-bottom:24px}

/* The app bar carries two chips (village, house); the spacer keeps settings
   pinned right whichever of them are shown. */
.liff .appbar .spacer{flex:1 1 auto;min-width:0}
.liff .village-chip{max-width:44vw}

.liff-nav{
  position:fixed;left:0;right:0;bottom:0;z-index:20;
  display:grid;grid-auto-flow:column;grid-auto-columns:1fr;
  gap:2px;padding:6px 6px calc(6px + env(safe-area-inset-bottom, 0px));
  background:var(--appbar-bg);
  -webkit-backdrop-filter:blur(20px) saturate(160%);
  backdrop-filter:blur(20px) saturate(160%);
  border-top:1px solid var(--appbar-border);
  box-shadow:0 -1px 0 var(--appbar-highlight) inset;
}
.liff-nav-item{
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
  padding:6px 2px;border:0;border-radius:var(--r-row);
  background:transparent;color:var(--text-2);
  font:inherit;font-size:var(--fs-caption);line-height:var(--lh-caption);
  text-decoration:none;cursor:pointer;
  transition:background 140ms var(--ease), color 140ms var(--ease);
}
.liff-nav-item .ico{width:20px;height:20px}
.liff-nav-item:hover{background:var(--hover);text-decoration:none}
.liff-nav-item:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.liff-nav-item[aria-current="page"]{color:var(--accent);font-weight:600}
.liff-nav-item[disabled]{opacity:.4;cursor:default}
.liff-nav-item[disabled]:hover{background:transparent}

/* Entry / link screens: one mark, one sentence, one action. */
.liff-mark{
  display:grid;place-items:center;
  width:52px;height:52px;margin:8px auto 12px;
  border-radius:50%;background:var(--selected);color:var(--accent);
}
.liff-mark .ico{width:26px;height:26px}
.liff-title{
  font-size:var(--fs-h2);line-height:var(--lh-h2);font-weight:600;
  color:var(--text);text-align:center;
}
/* Loose on purpose: Thai stacks vowels and tone marks two levels high, and the
   20px of the body ramp collides once a sentence wraps. */
.liff-note{
  margin-top:6px;font-size:var(--fs-body);line-height:1.6;color:var(--text-2);
}
.liff-note.center{text-align:center;color:var(--text-3)}
.liff-title + .liff-note{text-align:center}
.liff-actions{display:flex;justify-content:center;margin-top:14px}

/* Every resident screen is one full-width card on a phone, and the footer is
   the last thing the thumb reaches. Left-aligned buttons under a card that
   wide read as an afterthought — the eye has to travel back for them — so the
   whole LIFF side centres its footer, pairs included (asked for on
   /liff/bills/{id}/pay, 2026-09-17). Submit stays first, so the order that
   tells the two buttons apart has not changed.
   Scoped to .liff: the admin side keeps the shared left-to-right footer. */
.liff .card-foot{justify-content:center}
/* …but a dialog is not a page: the notice box app.js builds on a LIFF screen
   is still a dialog, and dialog buttons keep the Fluent trailing edge. Needed
   because .modal-card comes earlier in this file and would otherwise lose the
   tie on specificity. */
.liff .modal-card .card-foot{justify-content:flex-end}

/* A footer with nothing to weigh its one button against gets more width to
   press: the gate screens (entry, link, simulate) and any card that opts in
   with .is-single. */
.liff.is-bare .card-foot .btn{min-width:180px}
.liff .card-foot.is-single .btn{min-width:200px}

/* The 10-digit code: wide, tabular, and big enough to check against a slip. */
.code-input{
  font-size:var(--fs-stat);line-height:var(--lh-stat);
  height:52px;text-align:center;
  font-variant-numeric:tabular-nums;letter-spacing:.18em;
}
.code-input::placeholder{letter-spacing:.18em;opacity:.35}

.liff-sim-list{display:flex;flex-wrap:wrap;gap:6px}

/* The one figure a screen is about — arrears on the home and bill screens, a
   balance on a fund, a budget on a project. Tone follows the number: owing
   money is warned about, owing nothing is good news and says so. */
.liff-hero{
  display:block;
  padding:14px 16px;
  border:1px solid var(--stroke-strong);
  border-radius:var(--r-card);
  background:var(--card-solid);
  color:var(--text);text-decoration:none;
  position:relative;
}
a.liff-hero:hover{background:var(--hover);text-decoration:none}
a.liff-hero:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.liff-hero .k{
  display:flex;align-items:center;gap:6px;flex-wrap:wrap;
  font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-2);
}
.liff-hero .v{
  display:block;margin-top:4px;
  font-size:var(--fs-stat);line-height:var(--lh-stat);font-weight:600;
  letter-spacing:-.4px;font-variant-numeric:tabular-nums;
}
.liff-hero .v small{font-size:var(--fs-body);font-weight:400;color:var(--text-2)}
.liff-hero .n{display:block;margin-top:2px;font-size:var(--fs-caption);line-height:1.6;color:var(--text-3)}
.liff-hero .chev{position:absolute;right:12px;top:50%;transform:translateY(-50%);color:var(--text-3)}
a.liff-hero{padding-right:34px}

.liff-hero.is-due{border-color:var(--warn);background:var(--warn-bg)}
.liff-hero.is-due .v{color:var(--warn)}
.liff-hero.is-clear{border-color:var(--ok);background:var(--ok-bg)}
.liff-hero.is-clear .v{color:var(--ok)}
.liff-hero.is-due .k .ico{color:var(--warn)}
.liff-hero.is-clear .k .ico{color:var(--ok)}
.liff-hero.is-plain .k .ico{color:var(--accent)}

/* Amount at the right of a list row. Tabular so a column of them lines up. */
.liff-amount{
  flex:0 0 auto;text-align:right;
  font-size:var(--fs-body);font-weight:600;
  font-variant-numeric:tabular-nums;color:var(--text);
}
.liff-amount small{
  display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);
  font-weight:400;color:var(--text-3);
}
.liff-amount.is-in{color:var(--ok)}
.liff-amount.is-out{color:var(--danger)}

/* How much of a budget is gone. Decorative — the figures beside it are the
   real answer, so it carries no text of its own. */
.liff-bar{
  display:block;margin-top:6px;
  height:4px;border-radius:2px;overflow:hidden;
  background:var(--stroke-strong);
}
.liff-bar > i{display:block;height:100%;background:var(--accent);border-radius:2px}
.liff-hero .liff-bar{margin-top:8px}
.row .liff-bar{margin-top:5px;max-width:180px}

/* Status filter strip. The chips share the full width of the list they filter:
   flex-grow lets them take whatever room is left, so the strip ends where the
   cards end at every width instead of trailing off mid-screen. They never
   shrink, so a set that does not fit (five chips on a small phone) still
   scrolls sideways rather than wrapping onto a second line, which would push
   the list below the fold. */
.liff-tabs{
  display:flex;gap:6px;
  overflow-x:auto;overscroll-behavior-x:contain;
  padding-bottom:2px;
  scrollbar-width:none;
}
.liff-tabs::-webkit-scrollbar{display:none}
.liff-tab{
  flex:1 0 auto;
  text-align:center;white-space:nowrap;
  padding:6px 12px;border-radius:999px;
  border:1px solid var(--stroke-strong);
  background:var(--card-solid);
  font-size:var(--fs-body);color:var(--text-2);text-decoration:none;
  transition:background 140ms var(--ease), color 140ms var(--ease);
}
.liff-tab:hover{background:var(--hover);text-decoration:none}
.liff-tab:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.liff-tab[aria-current="page"]{
  background:var(--selected);border-color:var(--accent);
  color:var(--accent);font-weight:600;
}

/* ============================================================
   8. DESKTOP ( >= 860px )
   ============================================================ */
@media (min-width:860px){
  .hamburger{display:none}
  .desk-only{display:grid}

  /* Sign-in card opens into its two panels. */
  .auth-card{
    max-width:880px;
    grid-template-columns:1fr 1fr;
  }
  .auth-aside{
    display:flex;flex-direction:column;gap:18px;
    padding:32px 28px 0;
    background:
      radial-gradient(420px 260px at 12% 0%, var(--selected), transparent 70%),
      var(--surface-alt);
    border-right:1px solid var(--stroke);
  }
  .auth-main{padding:32px 30px 22px}
  /* The brand block repeats what the welcome panel already says. */
  .auth-main .auth-brand{display:none}

  .appbar{
    top:0;margin:0;padding:8px 12px;border-radius:0;
    border:0;border-bottom:1px solid var(--appbar-border);
    background:var(--appbar-bg);box-shadow:none;
  }

  .drawer-clip{display:contents}

  .sidebar{
    position:sticky;z-index:auto;
    top:47px;left:auto;
    align-self:flex-start;
    flex:0 0 var(--sidebar-w);
    width:var(--sidebar-w);max-width:none;
    height:calc(100dvh - 47px);
    transform:none;
    background:transparent;
    -webkit-backdrop-filter:none;backdrop-filter:none;
    box-shadow:none;
    border:0;border-radius:0;
    border-right:1px solid var(--stroke-strong);
    overflow-y:auto;overscroll-behavior:contain;
  }
  .scrim{display:none !important}

  .content{padding:4px 20px 28px}

  .form-grid.cols-2{grid-template-columns:1fr 1fr}
  .village-chip{max-width:280px}

  /* The resident side is a phone screen wherever it is opened. On a desktop
     browser — which is where it gets built and reviewed — it keeps its phone
     width and centres, rather than stretching a bottom nav across 1400px. */
  .liff .shell,
  .liff .appbar{max-width:520px;margin-left:auto;margin-right:auto;width:100%}
  .liff .appbar{border-radius:0 0 16px 16px;border:1px solid var(--appbar-border);border-top:0}
  .liff-nav{
    left:50%;transform:translateX(-50%);
    width:min(520px, 100%);
    border-left:1px solid var(--appbar-border);
    border-right:1px solid var(--appbar-border);
    border-radius:16px 16px 0 0;
  }
  .liff .village-chip{max-width:200px}

}

/* ============================================================
   9. REDUCED MOTION / PRINT
   ============================================================ */
@media (prefers-reduced-motion:reduce){
  *{transition-duration:1ms !important;animation-duration:1ms !important}
  .sidebar{transition-duration:var(--dur) !important}
  .scrim{transition-duration:var(--dur) !important}
}

@media print{
  .appbar,.sidebar,.drawer-clip,.scrim,.pagination,.filters,.form-actions,.liff-nav,.liff-tabs{display:none !important}
  body,.app{background:#fff}
  .card{box-shadow:none;border:1px solid #ddd;background:#fff;backdrop-filter:none}
}

/* ============================================================
   BAR CHART — สองค่าเทียบกันรายเดือน (admin/arrears/show.php)
   Twelve columns of two bars: what was billed, and what came in. Plain CSS on
   design tokens — no chart library, so it inherits the theme, prints, and has
   nothing to load. The numbers themselves live on each bar's title and on the
   column's aria-label; the bars only carry the comparison.
   ============================================================ */
/* The legend rides in the card head next to the heading — two swatches and two
   words do not earn a row of their own above the chart. It has to reset the
   head's own type (h2 / 600) or it would read as a second heading. */
.barchart-legend{
  display:inline-flex;flex-wrap:wrap;gap:4px 16px;
  font-size:var(--fs-caption);line-height:var(--lh-caption);
  font-weight:400;color:var(--text-2);
}
.barchart-legend span{display:inline-flex;align-items:center;gap:6px}
.barchart-legend b{font-weight:600;color:var(--text);font-variant-numeric:tabular-nums}
.barchart-legend i{width:10px;height:10px;border-radius:2px;background:var(--accent)}
.barchart-legend i.paid{background:var(--ok)}
.barchart{
  position:relative;
  display:flex;align-items:flex-end;gap:2px;
  height:150px;margin-bottom:6px;padding-top:16px;
  border-bottom:1px solid var(--stroke-strong);
}
/* The top gridline and its value: without them a tall bar says nothing about
   how much money it stands for. */
.barchart::before{
  content:'';position:absolute;left:0;right:0;top:16px;
  border-top:1px dashed var(--stroke);
}
.barchart-max{
  position:absolute;left:0;top:0;
  font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-3);
  font-variant-numeric:tabular-nums;
}
.barchart .col{
  flex:1 1 0;min-width:0;height:100%;
  display:flex;flex-direction:column;align-items:center;gap:6px;
}
.barchart .bars{
  flex:1;min-height:0;width:100%;
  display:flex;align-items:flex-end;justify-content:center;gap:3px;
}
.barchart .bars > i{
  width:40%;max-width:15px;min-height:2px;
  border-radius:3px 3px 0 0;
  background:var(--accent);
  /* Printers drop backgrounds by default, which would leave the chart blank. */
  -webkit-print-color-adjust:exact;print-color-adjust:exact;
}
.barchart .bars > i.paid{background:var(--ok)}
/* Zero is drawn, not left out — but in the neutral stroke colour, so the 2px
   the bar cannot go below reads as "nothing came in" and not as a payment. */
.barchart .bars > i.is-zero{background:var(--stroke-strong)}
/* A month with no bill keeps its column and says so — an absent column would
   read as "the year is shorter", and "nothing was billed in May" is worth
   seeing on a screen about what a house owes. */
.barchart .col.is-empty .bars{
  border-bottom:2px dotted var(--stroke-strong);
  align-self:stretch;
}
.barchart .col .m{
  font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-3);
  white-space:nowrap;
}
.barchart .col.is-empty .m{opacity:.55}
.barchart .col .m .num{display:none}
@media (max-width:520px){
  /* Twelve Thai month names do not fit across a phone — "มิ.ย." alone is most
     of a column. The axis switches to month numbers rather than shrinking the
     type below the ramp's smallest step; the full name is still in the
     column's aria-label either way. */
  .barchart{gap:1px}
  .barchart .col .m .name{display:none}
  .barchart .col .m .num{display:inline}
  .barchart .bars > i{width:44%}
}

/* ============================================================
   PRINTABLES — layouts/print.php + admin/invoices/print.php
                             + admin/payments/receipt.php
   A page that exists to become paper: no app shell, one .sheet per document,
   and a page break between them. Sized in mm because that is what the paper
   is. Bills and receipts share every class here — they are the same kind of
   object, and a receipt that looked like a different system would be read as
   coming from one.
   ============================================================ */
body.printable{background:var(--surface);padding:24px 16px}
.print-bar{
  display:flex;gap:8px;justify-content:flex-end;
  max-width:190mm;margin:0 auto 16px;
}
.sheet{
  box-sizing:border-box;
  width:100%;max-width:190mm;margin:0 auto 16px;
  padding:16mm 14mm;
  background:#fff;color:#111;
  border:1px solid var(--stroke);border-radius:var(--r-card);
  font-size:var(--fs-body);line-height:1.55;
}
.sheet-head{display:flex;gap:16px;justify-content:space-between;align-items:flex-start;flex-wrap:wrap}
.sheet-org{font-size:var(--fs-h2);line-height:var(--lh-h2);font-weight:600}
.sheet-sub{font-size:var(--fs-caption);line-height:1.6;color:#555}
.sheet-title{text-align:right;flex:0 0 auto}
.sheet-title .t{font-size:var(--fs-title);line-height:var(--lh-title);font-weight:600}
.sheet-title .no{font-size:var(--fs-body);color:#444;font-variant-numeric:tabular-nums}
/* A cancelled bill has to be unmistakable on paper — nobody can hover a sheet
   to check its status. */
.sheet-void{
  display:inline-block;margin-top:6px;padding:2px 10px;
  border:2px solid var(--danger);border-radius:4px;
  color:var(--danger);font-weight:600;letter-spacing:.5px;
}
.sheet-meta{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 24px;
  margin:14mm 0 8mm;padding:10px 0;
  border-top:1px solid #ddd;border-bottom:1px solid #ddd;
}
.sheet-meta .k{display:inline-block;min-width:104px;color:#555;font-size:var(--fs-caption)}
.sheet-meta .v{font-weight:600}
/* A receipt exists to state one figure: the money that changed hands. It gets
   a box of its own above the breakdown, with the same amount in words — the
   digits on a receipt can be altered with a pen, the words make that visible.
   No background is relied on: printers drop them by default, the border does
   the work. */
.sheet-amount{
  display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 16px;
  margin:0 0 10mm;padding:10px 14px;
  border:1px solid #bbb;border-radius:6px;
}
.sheet-amount .k{font-size:var(--fs-caption);color:#555}
.sheet-amount .v{
  margin-left:auto;
  font-size:var(--fs-stat);line-height:var(--lh-stat);font-weight:600;
  font-variant-numeric:tabular-nums;
}
.sheet-amount .v small{font-size:var(--fs-body);font-weight:400;color:#555}
.sheet-amount .w{flex:1 1 100%;color:#333;line-height:1.6}
.sheet-items{width:100%;border-collapse:collapse;margin-bottom:10mm}
.sheet-items .sub{display:block;font-size:var(--fs-caption);color:#555;line-height:1.6}
.sheet-items th,.sheet-items td{padding:8px 6px;border-bottom:1px solid #eee;text-align:left}
.sheet-items thead th{
  font-size:var(--fs-caption);color:#555;font-weight:600;
  border-bottom:1px solid #bbb;
}
.sheet-items .c-num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.sheet-items tfoot th{font-weight:400;color:#555;border-bottom:0}
.sheet-items tfoot td{font-weight:600;border-bottom:0}
.sheet-items tfoot tr.due th,
.sheet-items tfoot tr.due td{
  font-size:var(--fs-h2);font-weight:600;color:#111;
  border-top:2px solid #111;padding-top:10px;
}
.sheet-note{margin-bottom:8mm;color:#444}
.sheet-foot{
  display:flex;gap:16px;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;
  margin-top:14mm;font-size:var(--fs-caption);color:#555;
}
.sheet-sign{display:flex;flex-direction:column;gap:6px;align-items:center;min-width:56mm}
.sheet-sign span:first-child{display:block;width:100%;border-bottom:1px dotted #888;height:14mm}

@media print{
  @page{size:A4;margin:0}
  body.printable{background:#fff;padding:0}
  .print-bar{display:none !important}
  .sheet{
    max-width:none;width:auto;margin:0;
    border:0;border-radius:0;
    /* Last sheet must not push a blank page out of the printer. */
    break-after:page;page-break-after:always;
  }
  .sheet:last-child{break-after:auto;page-break-after:auto}
}
