/* Material Design 3 — baseline scheme.
 *
 * Everything visual is expressed as MD3 system tokens so the Material Web
 * components and the surrounding layout stay in step: the components read
 * --md-sys-color-* themselves, and the app chrome below reads the same values.
 *
 * Fonts are served from vendor/ rather than fonts.googleapis.com so the app has
 * no cross-origin dependency and works on first launch with no signal.
 */

@import url('vendor/fonts.css');

:root {
  /* --- MD3 baseline, light --- */
  --md-sys-color-primary: #6750A4;
  --md-sys-color-on-primary: #FFFFFF;
  --md-sys-color-primary-container: #EADDFF;
  --md-sys-color-on-primary-container: #21005D;
  --md-sys-color-secondary: #625B71;
  --md-sys-color-on-secondary: #FFFFFF;
  --md-sys-color-secondary-container: #E8DEF8;
  --md-sys-color-on-secondary-container: #1D192B;
  --md-sys-color-tertiary: #7D5260;
  --md-sys-color-on-tertiary: #FFFFFF;
  --md-sys-color-tertiary-container: #FFD8E4;
  --md-sys-color-on-tertiary-container: #31111D;
  --md-sys-color-error: #B3261E;
  --md-sys-color-on-error: #FFFFFF;
  --md-sys-color-error-container: #F9DEDC;
  --md-sys-color-on-error-container: #410E0B;
  --md-sys-color-background: #FEF7FF;
  --md-sys-color-on-background: #1D1B20;
  --md-sys-color-surface: #FEF7FF;
  --md-sys-color-on-surface: #1D1B20;
  --md-sys-color-surface-variant: #E7E0EC;
  --md-sys-color-on-surface-variant: #49454F;
  --md-sys-color-outline: #79747E;
  --md-sys-color-outline-variant: #CAC4D0;
  --md-sys-color-surface-container-lowest: #FFFFFF;
  --md-sys-color-surface-container-low: #F7F2FA;
  --md-sys-color-surface-container: #F3EDF7;
  --md-sys-color-surface-container-high: #ECE6F0;
  --md-sys-color-surface-container-highest: #E6E0E9;
  --md-sys-color-inverse-surface: #322F35;
  --md-sys-color-inverse-on-surface: #F5EFF7;
  --md-sys-color-shadow: #000000;
  --md-sys-color-scrim: #000000;

  /* F-44. The two ends of a class's ride ranking, and the app's only colours
     outside the MD3 scheme above. They are not part of it because MD3 has no
     "good / bad" pair -- error is the nearest thing and it means something
     else -- and because these two have one job: to be legible as a tint
     behind a row of ordinary text, in a sheet drawn on `surface`. Light
     values here, dark ones in the media query below; a single pastel cannot
     do both. */
  --rank-top-bg: #DDF3E0;
  --rank-bot-bg: #FBE0DE;

  --md-ref-typeface-brand: 'Roboto', 'Noto Sans Hebrew', system-ui, sans-serif;
  --md-ref-typeface-plain: 'Roboto', 'Noto Sans Hebrew', system-ui, sans-serif;

  /* MD3 shape scale */
  --shape-xs: 4px;
  --shape-s: 8px;
  --shape-m: 12px;
  --shape-l: 16px;
  --shape-xl: 28px;
}

@media (prefers-color-scheme: dark) {
  :root {
    /* --- MD3 baseline, dark --- */
    --md-sys-color-primary: #D0BCFF;
    --md-sys-color-on-primary: #381E72;
    --md-sys-color-primary-container: #4F378B;
    --md-sys-color-on-primary-container: #EADDFF;
    --md-sys-color-secondary: #CCC2DC;
    --md-sys-color-on-secondary: #332D41;
    --md-sys-color-secondary-container: #4A4458;
    --md-sys-color-on-secondary-container: #E8DEF8;
    --md-sys-color-tertiary: #EFB8C8;
    --md-sys-color-on-tertiary: #492532;
    --md-sys-color-tertiary-container: #633B48;
    --md-sys-color-on-tertiary-container: #FFD8E4;
    --md-sys-color-error: #F2B8B5;
    --md-sys-color-on-error: #601410;
    --md-sys-color-error-container: #8C1D18;
    --md-sys-color-on-error-container: #F9DEDC;
    --md-sys-color-background: #141218;
    --md-sys-color-on-background: #E6E0E9;
    --md-sys-color-surface: #141218;
    --md-sys-color-on-surface: #E6E0E9;
    --md-sys-color-surface-variant: #49454F;
    --md-sys-color-on-surface-variant: #CAC4D0;
    --md-sys-color-outline: #938F99;
    --md-sys-color-outline-variant: #49454F;
    --md-sys-color-surface-container-lowest: #0F0D13;
    --md-sys-color-surface-container-low: #1D1B20;
    --md-sys-color-surface-container: #211F26;
    --md-sys-color-surface-container-high: #2B2930;
    --md-sys-color-surface-container-highest: #36343B;
    --md-sys-color-inverse-surface: #E6E0E9;
    --md-sys-color-inverse-on-surface: #322F35;

    /* F-44. Dark counterparts of the two band tints: the same two hues, dark
       enough to sit under #E6E0E9 body text at the contrast the rest of this
       sheet keeps, rather than the light pastels dimmed -- a pastel on a
       #141218 surface reads as a white box with a colour cast. */
    --rank-top-bg: #1F3A28;
    --rank-bot-bg: #45231F;
  }
}

* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--md-sys-color-background);
  color: var(--md-sys-color-on-background);
  font-family: var(--md-ref-typeface-plain);
  overflow-x: hidden;   /* on <body>, not #app — see #app comment below */
}

/* The horizontal-overflow guard lives on <body>, not here: the browser
 * propagates a root/body overflow to the viewport itself and leaves <body>
 * computing as 'visible', so it never becomes a scroll container. The same
 * rule on #app would compute #app's own overflow-y to 'auto' (the CSS
 * overflow spec forces the other axis off 'visible' once one axis isn't),
 * making #app — which never actually scrolls, the page does — the "nearest
 * scrolling ancestor" for every position:sticky appbar below. That silently
 * breaks every sticky appbar in the app: it stops sticking and just scrolls
 * away with the content, which is what F-17's trip-name/gear row surfaced.
 */
#app {
  direction: rtl;
  max-width: 460px;
  margin: 0 auto;
  min-height: 100vh;
  position: relative;
  padding-bottom: 96px;              /* clears the navigation bar */
  background: var(--md-sys-color-surface);
}

/* ---------- Material Symbols ---------- */
.msi {
  font-family: 'Material Symbols Outlined';
  font-weight: normal;
  font-style: normal;
  font-size: 24px;
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  display: inline-block;
  white-space: nowrap;
  direction: ltr;
  -webkit-font-feature-settings: 'liga';
  font-feature-settings: 'liga';
  -webkit-font-smoothing: antialiased;
  user-select: none;
}

/* ---------- top app bar (MD3 small) ---------- */
.appbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--md-sys-color-surface);
  padding: 12px 16px 10px;
  border-bottom: 1px solid var(--md-sys-color-surface-variant);
}
.appbar-row { display: flex; align-items: flex-start; gap: 12px; }
.appbar h1 {
  flex: 1;
  min-width: 0;
  font-size: 22px;
  line-height: 28px;
  font-weight: 400;
  color: var(--md-sys-color-on-surface);
}
.appbar .sub {
  font-size: 14px;
  line-height: 20px;
  letter-spacing: .25px;
  color: var(--md-sys-color-on-surface-variant);
  margin-top: 2px;
}
.appbar-row.trip-row { align-items: center; margin-bottom: 6px; }
.trip-name {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  line-height: 16px;
  font-weight: 500;
  letter-spacing: .1px;
  color: var(--md-sys-color-on-surface-variant);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* F-34. A button that has to look exactly like the text it replaces, plus a
   chevron: the trip name is the app's title, not a control, and only becomes
   one for a family whose link reaches more than one trip. */
.trip-name.tripswitch{background:none;border:0;padding:0;font:inherit;color:inherit;
  display:flex;align-items:center;gap:2px;cursor:pointer;max-width:100%}
.trip-name.tripswitch .msi{font-size:18px;flex:none;opacity:.7}

/* Sync state. Quiet when healthy, error-coloured when not — the only thing
   worth interrupting anyone for is "your change did not save". */
.sync {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; line-height: 16px; margin-top: 6px;
  color: var(--md-sys-color-on-surface-variant);
}
.sync .msi { font-size: 16px; }
.sync.off { color: var(--md-sys-color-error); }

/* ---------- layout ---------- */
/* overflow-x: hidden here, not on #app or .appbar — .pane is a sibling of
 * .appbar (never its ancestor), so this clips a stray wide descendant (e.g. a
 * form field) without turning #app back into a scroll container that would
 * break position:sticky on .appbar again (see the #app comment above). */
.pane { padding: 12px 16px 8px; overflow-x: hidden; }
/* Two edge screens (the trip picker, and a trip with no days yet) reach for
   this name. It had no rule at all, so both rendered flush to the screen edge
   while every other body in the app sat inside .pane's 16px gutter. Same
   padding, own name: these two are not tab bodies and do not want .pane's
   growing list of tab-body concerns. */
.wrap { padding: 12px 16px 8px; overflow-x: hidden; }
.days { overflow-x: hidden; }

h2.sec {
  font-size: 14px;
  line-height: 20px;
  letter-spacing: .1px;
  font-weight: 500;
  color: var(--md-sys-color-primary);
  margin: 22px 4px 10px;
}
h2.sec:first-child { margin-top: 8px; }

/* A section header that is also the control collapsing its list (F-6).
   Deliberately the same type, colour and margins as h2.sec — the tab is a run
   of sections and these two must not read as a different kind of thing. */
.sec-toggle {
  position: relative;                /* md-ripple */
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  margin: 22px 0 10px;
  padding: 6px 4px;
  min-height: 48px;                  /* MD3 minimum touch target */
  border: 0; background: none; cursor: pointer;
  border-radius: var(--shape-s);
  font-family: inherit; text-align: start;
  color: var(--md-sys-color-primary);
  -webkit-tap-highlight-color: transparent;
}
.sec-toggle .sec-t {
  font-size: 14px; line-height: 20px; letter-spacing: .1px; font-weight: 500;
}
/* The count, in the pill shape the rest of the app uses for a status label.
   It is the whole point of a collapsed header: without it a closed section and
   an empty one are the same two words. */
.sec-toggle .sec-n {
  flex: 0 0 auto;
  font-size: 12px; font-weight: 500; line-height: 16px;
  font-variant-numeric: tabular-nums;
  padding: 2px 8px; border-radius: 8px;
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  /* Title and count read as one unit; everything after them goes to the far
     end. The auto margin lives here and not on .chev because .chev is an .msi,
     which sets direction:ltr on itself — inline-start/end on that element
     resolve against ltr and land on the wrong side inside this rtl row. */
  margin-inline-end: auto;
}
/* Far (logical) end of the row, like the chevron on a .row. */
.sec-toggle .chev {
  flex: 0 0 auto;
  font-size: 22px;
  transform: rotate(-90deg);         /* chevron_left → pointing down: closed */
  transition: transform .18s ease;
}
.sec-toggle .chev.up { transform: rotate(90deg); }
/* F-18: a list with 5 or fewer items renders this same header as a <div> —
   count shown, nothing to expand into, so no pointer cursor or tap feedback. */
div.sec-toggle { cursor: default; }
@media (prefers-reduced-motion: reduce) {
  .sec-toggle .chev { transition: none; }
}

/* The search field above a filterable list (F-8). Sits between the section
   header and the rows, tight to both: it belongs to that list, and a normal
   .stack gap here would read as a third section of its own. */
.filter { margin: 0 0 10px; }
.filter md-outlined-text-field { width: 100%; }
/* The clear button is always in the DOM — see vFilterField for why it is hidden
   rather than added and removed. visibility, not display: the field keeps the
   trailing icon's width reserved either way, so the text never shifts sideways
   under the caret on the first or last keystroke. */
.filter.is-empty md-icon-button[slot="trailing-icon"] {
  visibility: hidden;
  pointer-events: none;
}

/* Cards: MD3 filled-card behaviour without pulling in the labs component. */
.card {
  position: relative;
  background: var(--md-sys-color-surface-container-low);
  border-radius: var(--shape-m);
  padding: 16px;
}
.card + .card { margin-top: 12px; }
.card-title {
  font-size: 11px; font-weight: 500; letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant);
  margin-bottom: 12px;
}

.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.row3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
.stack > * + * { margin-top: 12px; }

md-outlined-text-field, md-outlined-select { width: 100%; }

.empty {
  border: 1px dashed var(--md-sys-color-outline-variant);
  border-radius: var(--shape-m);
  padding: 24px 16px;
  text-align: center;
  color: var(--md-sys-color-on-surface-variant);
  font-size: 14px;
  line-height: 22px;
}

.hint {
  font-size: 12px;
  line-height: 18px;
  color: var(--md-sys-color-on-surface-variant);
  padding: 0 4px;
  margin-top: 10px;
}

.banner {
  display: flex; gap: 12px; align-items: flex-start;
  background: var(--md-sys-color-tertiary-container);
  color: var(--md-sys-color-on-tertiary-container);
  border-radius: var(--shape-m);
  padding: 12px 14px;
  font-size: 13px; line-height: 20px;
}
.banner.warn {
  background: var(--md-sys-color-error-container);
  color: var(--md-sys-color-on-error-container);
}
.banner .msi { font-size: 20px; flex: 0 0 auto; }

/* ---------- day strip ---------- */
.days { padding: 4px 8px 0; }
md-tabs { --md-primary-tab-container-color: transparent; }

/* ---------- list rows ---------- */
md-list { background: transparent; --md-list-container-color: transparent; }
.row {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 4px;
  border-bottom: 1px solid var(--md-sys-color-surface-variant);
}
.row:last-child { border-bottom: 0; }
.row .grow { flex: 1; min-width: 0; }
/* display:block on both, and it is load-bearing rather than cosmetic. As plain
   inline spans these two flow as one continuous run, so a row rendered as
   "מוניות לפארק" + "היום · משפחה 1 שילמו" put the first words of the subtitle on
   the end of the title line, with nothing between them. The 2px margin below
   only ever made sense for a block, which is what these were always meant to
   be — a title with a subtitle under it. */
.row .t1 {
  display: block;
  font-size: 16px; line-height: 22px; letter-spacing: .15px;
  color: var(--md-sys-color-on-surface);
}
.row .t2 {
  display: block;
  font-size: 13px; line-height: 18px; letter-spacing: .25px;
  color: var(--md-sys-color-on-surface-variant);
  margin-top: 2px;
}
/* A document's own description (F-7), on its own line between the file name and
   the size/visibility line. Same size as .t2 but in the full on-surface colour:
   it is content someone typed, not metadata the app derived. `anywhere` because
   this is the one string in a row a user writes freely — a 120-character run
   with no space in it would otherwise push the row past 375px, and the same
   guard on .t1 covers the camera-roll file names that can do it too. */
.row .t1, .row .t2 { overflow-wrap: anywhere; }
.row .t2.desc { color: var(--md-sys-color-on-surface); }
.row .num {
  font-variant-numeric: tabular-nums;
  text-align: left; white-space: nowrap;
  font-size: 15px; font-weight: 500;
}
.row .num small {
  display: block; font-size: 12px; font-weight: 400;
  color: var(--md-sys-color-on-surface-variant);
}
/* "We owe" on the balance card. The error colour rather than the error
   container pair used by .lbl.hot — that one fills a pill background, and
   there is no pill here. */
.row .num.hot { color: var(--md-sys-color-error); }

/* Family colour marker. Kept as a plain dot: Material has no "identity colour"
   token, and five families need telling apart at a glance. */
.dot-fam {
  width: 12px; height: 12px; border-radius: 50%;
  flex: 0 0 auto; border: 1px solid rgba(0,0,0,.15);
}

.avatar {
  width: 40px; height: 40px; border-radius: 50%; flex: 0 0 40px;
  display: flex; align-items: center; justify-content: center;
  font-size: 16px; font-weight: 500;
  background: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
}

/* F-36. The one control the multi-trip shell is built from: a trip, drawn as a
   tappable tile, on the picker screen, in the switcher sheet and in settings.
   It is deliberately NOT .row — .row is a rule for a <div>, and F-34's first
   cut put it on a <button>, where the user agent then supplied a border, a
   background, a system font and a shrink-to-fit width that .row never
   overrides. Everything a <button> has to reset is reset here, once. */
.trip-list { display: flex; flex-direction: column; gap: 10px; }
.tripcard {
  position: relative; display: flex; align-items: center; gap: 14px;
  width: 100%; appearance: none; -webkit-appearance: none;
  background: var(--md-sys-color-surface-container);
  border: 1px solid transparent;
  border-radius: var(--shape-l);
  padding: 14px 16px;
  font: inherit; color: inherit; text-align: right;
  cursor: pointer; overflow: hidden;
}
/* The trip you are on. A filled primary container rather than an outline: at a
   glance on a 375px screen, "which one am I in" has to be answerable from the
   fill, not from a 1px edge. */
.tripcard.on {
  background: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
  border-color: var(--md-sys-color-primary);
}
.tripcard-ic {
  width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--md-sys-color-surface-container-highest);
  color: var(--md-sys-color-on-surface-variant);
}
.tripcard.on .tripcard-ic {
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
}
.tripcard-ic .msi { font-size: 22px; }
.tripcard-tx { flex: 1; min-width: 0; }
/* A trip name is user-typed and can be long; one line with an ellipsis rather
   than a wrap, so a card's height never depends on what someone called it. */
.tripcard-n {
  display: block; font-size: 16px; line-height: 22px; letter-spacing: .15px;
  font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tripcard-s {
  display: block; font-size: 13px; line-height: 18px; letter-spacing: .25px;
  margin-top: 2px;
  color: var(--md-sys-color-on-surface-variant);
}
/* On the filled card the -variant grey has no contrast left, so the subtitle
   takes the container's own on-colour and steps back with opacity instead. */
.tripcard.on .tripcard-s { color: inherit; opacity: .78; }
.tripcard-ck { flex: 0 0 auto; font-size: 22px; }

/* ---------- activities ---------- */
.act { display: flex; gap: 14px; padding: 12px 4px; border-bottom: 1px solid var(--md-sys-color-surface-variant); }
.act:last-child { border-bottom: 0; }
.act .time {
  font-variant-numeric: tabular-nums;
  flex: 0 0 46px; padding-top: 1px;
  font-size: 14px; font-weight: 500;
  color: var(--md-sys-color-primary);
}
.act .grow { flex: 1; min-width: 0; }
.chips-row { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }

/* Compact non-interactive labels. md-suggestion-chip is interactive and 32px
   tall; these are read-only metadata, so a plain label reads better. */
.lbl {
  font-size: 11px; line-height: 16px; padding: 2px 8px;
  border-radius: var(--shape-s);
  background: var(--md-sys-color-surface-variant);
  color: var(--md-sys-color-on-surface-variant);
  white-space: nowrap;
}
/* F-39. One family's headcount on a class row ("כהן · 2"). Read-only
   metadata, so .lbl's 11px footprint rather than .leg's control-sized one --
   it sits in the same chips-row as the one real control there, and the size
   difference is what says which of them can be tapped. inline-flex only so
   the family's colour dot centres against the text; everything else is .lbl
   as it stands. */
.lbl.kidfam {
  display: inline-flex; align-items: center; gap: 5px;
  /* .lbl is nowrap, which is right for the fixed strings it was written for
     ("חייבים", "חדש") and wrong here: the text inside this one is a family
     name somebody typed, and a long one on a 375px row has nowhere to go.
     Wrapping inside the pill keeps the row intact whatever the name is --
     the same reasoning as .row .t1/.t2's overflow-wrap (app.css:377). */
  max-width: 100%; white-space: normal;
}
/* F-46. The two ends of a class's ride balance, on the class's own row.
   Built on .lbl.kidfam's shape -- same 11px pill, same inline-flex so a
   family dot centres against the text, same wrapping for a long name -- and
   differing in exactly one thing, the background, which is the same pair the
   ranking sheet tints its top and bottom rows with (--rank-top-bg /
   --rank-bot-bg). One pair of colours for one idea, in both places it is
   drawn: a reader who opens the sheet from this row must not have to learn a
   second colour language on arrival.
   on-surface rather than .lbl's on-surface-variant: these tints are lighter
   than the surface-variant .lbl assumes, and the muted text that reads well
   on grey goes thin on them. */
.lbl.rankend {
  display: inline-flex; align-items: center; gap: 5px;
  max-width: 100%; white-space: normal;
  color: var(--md-sys-color-on-surface);
}
.lbl.rankend.top { background: var(--rank-top-bg); }
.lbl.rankend.bot { background: var(--rank-bot-bg); }
/* The row of ends sits under the row of controls rather than beside them:
   these are the only two things on a class row that are neither a control
   nor a plain fact about the class, and mixing them into the chips-row above
   would put the ranking's answer in the same line as the button that opens
   it. 6px, not chips-row's own 8px -- they belong to the line above them. */
.chips-row.ends { margin-top: 6px; }

.lbl.hot { background: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); }
.lbl.ok  { background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); }
.lbl.info{ background: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
/* F-20's "חדש" mark. Filled primary rather than one of the containers above:
   it has to win against a row that may already be carrying חייבים or
   לאישורכם, and those two are the containers. It is also the only label here
   that goes away by itself, which is worth it looking unlike the ones that
   describe a lasting state. */
.lbl.new {
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
  font-weight: 500;
}

/* The payment-method control on an expense row (F-14), for the family that
   recorded it. A full-height tonal button beside an amount and a delete icon
   does not fit a 375px row, so it is compressed to roughly label height — the
   same footprint as the .lbl every other family sees in its place, which is
   what keeps the two versions of the row the same shape. */
md-filled-tonal-button.meth {
  flex: 0 0 auto;
  --md-filled-tonal-button-container-height: 30px;
  --md-filled-tonal-button-label-text-size: 12px;
  --md-filled-tonal-button-label-text-line-height: 16px;
  --md-filled-tonal-button-leading-space: 10px;
  --md-filled-tonal-button-trailing-space: 10px;
}

/* A SHARED expense row, which since F-14 carries more trailing controls than
   any other row in the app: the amount, the payment method, up to two status
   labels and a delete button. At 375px those leave the description a ~50px
   column that wraps one word per line, so this row alone is allowed to break —
   the description takes the first line to itself and the controls line up
   underneath it. Rows come out SHORTER this way, not taller: the wrapping they
   did inside a squeezed text column cost far more than one extra line.

   Private rows deliberately do NOT get this. They carry no status labels and no
   family dot, so the method control still fits beside a ~140px description and
   the row stays a single 67px line. */
.row.exp { flex-wrap: wrap; row-gap: 8px; }
.row.exp > .grow { flex: 1 0 100%; }

/* The private total broken out by payment method (F-14): directly under the
   heading and above the list it sums. Larger and darker than .hint because it
   is a figure rather than an aside — the cash number is the one somebody is
   standing at an ATM trying to read. The parts wrap instead of scrolling; a
   total plus three methods does not fit one 375px line. */
.subtotal {
  display: flex; flex-wrap: wrap; gap: 2px 12px;
  padding: 0 4px; margin: 0 0 10px;
  font-size: 13px; line-height: 20px;
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-on-surface-variant);
}
.subtotal .tot { color: var(--md-sys-color-on-surface); font-weight: 500; }

/* ---------- emergency card ---------- */
/* Deliberately the highest-contrast surface in the app: this gets held up to a
   stranger, outdoors, in Gulf sun. */
.ec {
  background: var(--md-sys-color-surface-container-highest);
  border-radius: var(--shape-l);
  padding: 24px 20px; text-align: center;
}
.ec .big { font-size: 32px; line-height: 40px; font-weight: 500; color: var(--md-sys-color-on-surface); }
.ec .meta { font-size: 14px; color: var(--md-sys-color-on-surface-variant); margin-top: 4px; }
.ec .lbl2 {
  font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant); margin: 18px 0 6px; font-weight: 500;
}
.ec .val { font-size: 16px; line-height: 24px; }
.tel {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);
  border-radius: var(--shape-xl);
  padding: 14px; font-size: 20px; font-weight: 500; text-decoration: none;
  font-variant-numeric: tabular-nums; direction: ltr; margin-top: 10px;
}

/* ---------- documents ---------- */
.filepick {
  display: block; text-align: center; cursor: pointer;
  border: 1px dashed var(--md-sys-color-outline);
  border-radius: var(--shape-m);
  padding: 20px 16px;
  color: var(--md-sys-color-on-surface-variant); font-size: 14px;
}
.filepick input { display: none; }
.filepick.has { border-style: solid; color: var(--md-sys-color-on-surface); }

/* ---------- terms ---------- */
.tos { font-size: 14px; line-height: 21px; color: var(--md-sys-color-on-surface-variant); }
.tos .tos-lead { font-size: 15px; color: var(--md-sys-color-on-surface); }
.tos h3 {
  font-size: 13px; font-weight: 500; letter-spacing: .5px;
  color: var(--md-sys-color-primary);
  margin: 16px 0 4px;
}
.tos p + h3 { margin-top: 18px; }

/* ---------- what's going on now ---------- */
.now-i {
  background: var(--md-sys-color-surface-container-high);
  border-radius: var(--shape-m);
  padding: 12px 14px;
}
.now-i + .now-i { margin-top: 8px; }
.now-h { display: flex; align-items: center; gap: 8px; }
.now-who { flex: 1; font-size: 14px; font-weight: 500; min-width: 0; }
.now-age { font-size: 12px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; }
.now-t { font-size: 16px; line-height: 22px; margin-top: 4px; }
/* Older than a few hours is probably no longer true. Faded rather than hidden:
   "they said the pool, four hours ago" is still worth something. */
.now-i.stale { background: var(--md-sys-color-surface-container-low); opacity: .65; }
.now-i .lbl .msi { font-size: 14px; vertical-align: -2px; }

/* ---------- worth knowing ---------- */
.note-i {
  background: var(--md-sys-color-surface-container-low);
  border-radius: var(--shape-m);
  padding: 14px 16px;
  border-inline-start: 4px solid var(--md-sys-color-outline-variant);
}
.note-i + .note-i { margin-top: 10px; }
.note-h { display: flex; align-items: flex-start; gap: 8px; }
.note-t { flex: 1; font-size: 16px; line-height: 22px; font-weight: 500; }
.note-b { font-size: 14px; line-height: 20px; color: var(--md-sys-color-on-surface-variant); margin-top: 6px; }
.note-i.k-closed { border-inline-start-color: var(--md-sys-color-error); }
.note-i.k-stay   { border-inline-start-color: var(--md-sys-color-primary); }
.note-i.k-do     { border-inline-start-color: var(--md-sys-color-tertiary); }
.note-i.k-info   { border-inline-start-color: var(--md-sys-color-outline); }
.note-i.now { background: var(--md-sys-color-surface-container-high); }

/* ---------- bingo ---------- */
.bg { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.bgi {
  position: relative; overflow: hidden;
  background: var(--md-sys-color-surface-container-low);
  border-radius: var(--shape-m);
  padding: 14px; text-align: start; min-height: 84px;
  display: flex; flex-direction: column;
  border: 1px solid transparent;
  color: var(--md-sys-color-on-surface);
  font-family: inherit;
}
.bgi .nm { flex: 1; font-size: 14px; line-height: 20px; }
.bgi[data-taken="1"] { background: var(--md-sys-color-surface-container-highest); }
.bgi[data-taken="1"] .nm { opacity: .55; text-decoration: line-through; }
.bgi[data-pending="1"] { border: 1px dashed var(--md-sys-color-tertiary); }
.bgi .by { display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 12px; font-weight: 500; }
.bgi .pts { margin-top: 8px; font-size: 12px; font-weight: 500; color: var(--md-sys-color-primary); }

.bar { height: 4px; border-radius: 2px; background: var(--md-sys-color-surface-variant); overflow: hidden; margin-top: 6px; }
.bar i { display: block; height: 100%; background: var(--md-sys-color-primary); }
.score { font-size: 20px; font-weight: 500; font-variant-numeric: tabular-nums; }

/* ---------- classes (F-29) ---------- */
/* A cancelled occurrence keeps its row rather than collapsing it (so the
   un-cancel control has something to look at) but has to read as cancelled at
   a glance -- opacity on the whole row rather than styling each piece inside
   it separately, since a row's contents already differ between the cancelled
   and live states (text legs vs. buttons) and this way the "cancelled" look
   is one rule that applies no matter which of those rendered. */
.class-row.cancelled { opacity: .55; }
/* The clock glyph ahead of the time, same treatment as the one already used
   ahead of a label inside .now-i (app.css:597) -- an .msi is sized for a
   24px touch target by default, which reads as oversized next to 13px text. */
.class-row .t2 .msi { font-size: 14px; vertical-align: -2px; margin-inline-end: 2px; }

/* One leg of one occurrence: a small pill, matching .lbl's footprint so a row
   with one leg as text and the other as a button (one family holds one leg,
   the other is open) does not change height between the two. Not .lbl itself
   -- .lbl is documented as compact READ-ONLY metadata (app.css:474), and a
   leg is exactly the thing that toggles between read-only and tappable
   depending on who holds it (classLegHtml() in app.js), so it gets its own
   name rather than stretching .lbl's contract to cover a control too. */
.leg {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; line-height: 16px;
  padding: 4px 8px;
  border-radius: var(--shape-s);
  background: var(--md-sys-color-surface-variant);
  color: var(--md-sys-color-on-surface-variant);
}
.leg-lbl { font-weight: 500; }
.leg-empty { color: var(--md-sys-color-error); }

/* F-40. One ride request and its answer, as one line.
   Its own row rather than more children of .chips-row, which is what held the
   two leg controls before it: chips-row wraps freely, and with several
   requests on one occurrence a wrap could put one family's pill next to
   another family's button -- two halves of different sentences reading as
   one. A request and its answer belong together and wrap together; several
   requests stack. The inner pieces are .leg and .legbtn unchanged, so a line
   whose answer is text and one whose answer is a button are the same
   height. */
.ride {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin-top: 8px;
}
/* The request half -- a button when it is ours (it opens the sheet that edits
   or withdraws it), a plain pill when it is anyone else's -- takes the free
   space, so the answer control sits on the same edge all the way down a stack
   of requests instead of stepping in and out with the length of each family's
   name. Written against the first child rather than against .leg so it holds
   whichever of the two shapes that half happens to be. */
.ride > :first-child { margin-inline-end: auto; }

/* The take/release controls for an open or mine leg. Real md-* components,
   not a bare <button> reset to look like .leg -- B-5 was exactly that mistake
   made against .row, and the fix there (see .tripcard, app.css:408) was to
   either reset every user-agent default by hand or use a component that
   never had them. These shrink an existing Material button down to .leg's
   footprint instead, the same move .meth already makes for the expense row's
   payment-method control (app.css:497). */
md-outlined-button.legbtn, md-filled-tonal-button.legbtn {
  flex: 0 0 auto;
  --md-outlined-button-container-height: 28px;
  --md-filled-tonal-button-container-height: 28px;
  --md-outlined-button-label-text-size: 12px;
  --md-filled-tonal-button-label-text-size: 12px;
  --md-outlined-button-label-text-line-height: 16px;
  --md-filled-tonal-button-label-text-line-height: 16px;
  --md-outlined-button-leading-space: 10px;
  --md-filled-tonal-button-leading-space: 10px;
  --md-outlined-button-trailing-space: 10px;
  --md-filled-tonal-button-trailing-space: 10px;
}
/* The dot inside a "mine" leg button is 12px against a 16px line-height text
   button; without this it sits a hair low next to the family name beside it. */
md-filled-tonal-button.legbtn .dot-fam { vertical-align: -1px; }

/* ---------- the history sheet (F-45) ---------- */
/* A past week is the same .row/.class-row as an upcoming one -- same date
   line, same clock glyph, same cancelled treatment -- so almost nothing is
   needed here. What differs is that nothing on the line is a control: a past
   ride is a statement, and the two halves of it want to read as one sentence
   rather than as a request and an answer facing each other across the row.
   So .ride.past drops the `margin-inline-end: auto` that pushes those two
   apart, and the driver follows the family it drove for. */
.ride.past > :first-child { margin-inline-end: 0; }
.ride.past { gap: 8px; }
/* The driver half is plain text rather than a pill, and that is exactly what
   breaks the family dot on it: .dot-fam sets a width and a height but no
   display, so it is sized only where it happens to be a FLEX ITEM (.leg is
   inline-flex, .row is flex). Dropped into a .t2, which .row .t2 makes a
   plain block, it is a non-replaced inline element -- width and height do
   not apply, and all that renders is its own 1px border, as a hairline. The
   inline-flex here is what gives it a box, and is the same shape .leg uses
   one line up rather than a second way of solving it. */
.ride.past .t2 { display: inline-flex; align-items: center; gap: 6px; }
/* "Nobody drove" is a fact, not a fault -- the same distinction .leg-empty
   makes on an upcoming week, where an unanswered request IS something to act
   on and is drawn in the error colour. Here the week is over and there is
   nothing to do about it, so it takes the muted colour every other piece of
   metadata on the row takes. */
.ride.past .nodrv, .class-row.past .quiet { color: var(--md-sys-color-outline); }
/* The correction form, opened under the week it corrects. A card inside the
   dialog's own scroller rather than a second dialog: the app has no stacked
   dialogs anywhere (see ui.payForm's own note in app.js), and two of them at
   375px is a scroll trap. The negative inline margin is what .fixform needs
   to line its own edge up with the rows above it, since a .card inside
   slot="content" is otherwise inset by the dialog's padding twice. */
.fixform { margin: 8px 0 12px; }
.fixform .card-title { margin-bottom: 2px; }
/* The chips wrap freely here, unlike the day chips on the add form which are
   a fixed seven: a group of six families is two rows of chips on a phone, and
   a horizontal scroller would hide the family somebody is looking for behind
   an edge with no affordance. */
.fixform md-chip-set { flex-wrap: wrap; }

/* ---------- the ranking sheet (F-43) ---------- */
/* One bar, two readings: the solid part is what a family has already driven,
   the translucent tail is what it is booked to drive. A SECOND element rather
   than a gradient or a border on .bar i, because the two lengths are two
   numbers the row has to be able to state separately -- and because the tail
   carries the family's own colour at a lower opacity, which is what keeps a
   row readable as one family rather than as a colour and a grey.
   .bar's own rule (app.css:650) leaves `i` as a block, which stacks two
   children vertically; the flex here is the whole difference between this
   variant and the plain one the bingo table and the storage meter use. */
/* F-44 took the flexbox out. A score can be negative now, so a segment is no
   longer "the next thing in the row" but an interval on a scale that contains
   zero somewhere inside it -- which is a position and a width, not an order.
   Both segments are placed with inset-inline-start, so the track runs in the
   page's own direction (right-to-left here: the bigger the score, the further
   left the bar reaches) with no RTL special case anywhere in app.js. */
.bar.split { position: relative; overflow: visible; }
/* overflow is visible rather than .bar's own hidden, so the zero tick below
   can stand a little proud of the track -- a 1px line 4px tall, flush inside
   it, is not a mark anybody sees on a phone. The segments take the radius
   .bar's clipping used to give them, so nothing about them changes. */
.bar.split i, .bar.split b {
  position: absolute; top: 0; height: 100%; border-radius: 2px;
}
/* Where zero is, drawn only when something in the table is actually negative
   (app.js decides that, not this file). It reads as a line and not as a bar:
   1px wide, on-surface rather than any family's colour, and not quite opaque,
   so a bar that ends exactly on it still shows its own edge. */
.bar.split u.zero {
  position: absolute; top: -3px; width: 1px; height: calc(100% + 6px);
  background: var(--md-sys-color-on-surface-variant); opacity: .7;
}
/* .32 rather than a fixed lighter colour: a per-family colour has no "lighter
   version" the app knows about, and a fixed tint would read differently
   against the light and dark surfaces this sheet is drawn on. */
.bar.split b { opacity: .32; }

/* A rank row carries more pieces than an ordinary .row -- place, sometimes a
   move chip, a dot, a name over a bar, and two numbers -- so it takes .row's
   14px gap down to 8px. Without this the two-digit case ("12 → 18" beside a
   long family name) wraps the numbers under the bar at 375px, which is the
   one thing this row must never do: the pair of numbers IS the second rank. */
.row.rk { gap: 8px; }
/* F-44. The two ends of the table, tinted rather than badged: the row is
   already carrying a place, sometimes a move chip, a dot, a name, a bar and
   two numbers, and a seventh piece would be the one that makes it wrap at
   375px. A background is free of width.
   The colours are two tokens of their own (see :root) rather than
   --md-sys-color-error-container and some green beside it: the error
   container is the app's "something is wrong" surface and being last in a
   lift ranking is not an error, only the other end of a list. Both are mixed
   toward the surface they sit on, which is what keeps them readable as a
   tint and not as a highlighter in either theme.
   The tint takes the row's own box exactly -- no padding of its own and no
   negative margin to widen it. Both were tried: a wider band is prettier on
   its own and overflows the dialog it is drawn in (measured at 375px: the
   row box is 15..360 and the dialog's container is the same 15..360, so any
   bleed lands outside the rounded corner), and inline padding without the
   margin shifts a tinted row's text out of line with the untinted rows above
   and below it. `.row`'s own 12px/4px padding is the breathing room. */
.row.rk.top, .row.rk.bot { border-radius: var(--shape-s); }
.row.rk.top { background: var(--rank-top-bg); }
.row.rk.bot { background: var(--rank-bot-bg); }
.row.rk .place {
  flex: 0 0 auto; min-width: 14px; text-align: center;
  font-size: 14px; font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-on-surface-variant);
}
/* Where a family is heading, shown only where that differs from where it is.
   Primary rather than a red/green pair -- and F-44, which DID bring a
   red/green pair to this sheet, is what makes the distinction worth keeping:
   the two bands say where the table stands today, which is a fact, while this
   chip says where a family is heading if every booked lift happens, which is
   a forecast. Colouring a forecast would be the app taking a side on a thing
   nobody has done yet. */
.row.rk .move {
  flex: 0 0 auto;
  font-size: 11px; line-height: 16px; font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-primary);
}
/* .score is 20px where the bingo table uses it, which is a screen with one
   number per row; two numbers and an arrow at that size push a 375px row into
   a wrap. Same weight and the same tabular figures, two steps down. */
.row.rk .score { font-size: 15px; }
.row.rk .score .arr {
  font-size: 13px;
  color: var(--md-sys-color-on-surface-variant);
  margin: 0 2px;
}
/* What the two halves of a bar mean, in two words each, once at the bottom
   rather than on every row. The swatches are the bar's own two treatments at
   the size of a full stop, so the legend cannot drift from what it explains. */
.ranklegend {
  display: flex; gap: 14px; flex-wrap: wrap;
  margin-top: 12px; padding-top: 10px;
  border-top: 1px solid var(--md-sys-color-surface-variant);
  font-size: 12px; color: var(--md-sys-color-on-surface-variant);
}
.ranklegend span { display: inline-flex; align-items: center; gap: 6px; }
.ranklegend i, .ranklegend b, .ranklegend em {
  width: 14px; height: 6px; border-radius: 3px;
  background: var(--md-sys-color-primary);
}
.ranklegend b { opacity: .32; }
/* F-44's two bands, at the same size as the bar's own swatches and with the
   same rule behind them: the legend shows the treatment itself, so it cannot
   drift from what it explains. Taller than the bar swatches because a tint is
   a surface and a 6px sliver of one reads as a smudge. */
.ranklegend em { height: 12px; border: 1px solid var(--md-sys-color-outline-variant); }
.ranklegend em.top { background: var(--rank-top-bg); }
.ranklegend em.bot { background: var(--rank-bot-bg); }

/* ---------- navigation bar (MD3 spec, hand-built) ---------- */
/* The Material Web navigation bar lives in labs/ and is explicitly unstable, so
   this follows the MD3 navigation-bar spec directly: 80px tall, 32px indicator
   pill, 24px icon, 12px medium label, ripple on press. */
/* Finding 2 (F-29 fix wave): this was `display: grid; grid-template-columns:
   repeat(6, 1fr)`, which only ever looked right for `trip` mode's six tabs.
   `ongoing` mode (class/money/docs, F-29's MODE_TABS) draws three, and a
   fixed six-column grid left half the bar empty instead of splitting it
   among the tabs that actually exist. Flex instead of a grid driven by
   modeTabs().length from JS: the tab count is already known at render time,
   but there is no reason to thread it through an inline style when `flex: 1`
   on each .navitem gets the same result from CSS alone, self-adjusting for
   any future tab count without app.js needing to know this rule exists. */
.navbar {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 30;
  max-width: 460px; margin: 0 auto;
  display: flex;
  background: var(--md-sys-color-surface-container);
  padding-bottom: env(safe-area-inset-bottom);
  border-top: 1px solid var(--md-sys-color-surface-variant);
}
.navitem {
  position: relative;
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 12px 0 16px; border: 0; background: none; cursor: pointer;
  font-family: inherit; color: var(--md-sys-color-on-surface-variant);
  min-height: 64px;
}
.navitem .ind {
  display: flex; align-items: center; justify-content: center;
  width: 56px; height: 32px; border-radius: 16px;
  transition: background-color .15s ease;
}
.navitem .nl {
  font-size: 11px; line-height: 14px; font-weight: 500; letter-spacing: .3px;
  white-space: nowrap;
}
.navitem[aria-selected="true"] { color: var(--md-sys-color-on-surface); }
.navitem[aria-selected="true"] .ind {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}
@media (prefers-reduced-motion: reduce) { .navitem .ind { transition: none; } }
/* F-20's unread count, per the MD3 navigation-bar large-badge spec: pinned to
   the top-inline-end corner of the 56x32 indicator pill, not of the whole
   item, so it sits against the icon at every label length.
   `inset-inline-end` rather than `right` — this app is RTL, and the badge
   belongs on the same side of the icon in both directions. */
.navitem .badge {
  position: absolute; top: -2px; inset-inline-end: 6px;
  min-width: 16px; height: 16px; padding: 0 4px; box-sizing: border-box;
  border-radius: 8px;
  background: var(--md-sys-color-error); color: var(--md-sys-color-on-error);
  font-size: 11px; line-height: 16px; font-weight: 500;
  font-variant-numeric: tabular-nums; text-align: center;
  /* The pill is the positioning context, and it is 32px tall against a 24px
     icon — without this the badge is clipped by the ripple's overflow. */
  pointer-events: none;
}
.navitem .ind { position: relative; }

/* F-29's nav-bar gap warning — `class` tab only, drawn only when classGap()
   says so. Deliberately a plain dot with no number (unlike .badge just
   above): it answers "does something need a driver soon", not "how many",
   and a count here would invite reading it as an unread tally instead of a
   state to go look at.
   Positioned at the corner OPPOSITE .badge (bottom-inline-start vs..badge's
   top-inline-end) rather than the same one, so the two can sit on the same
   56x32 pill without overlapping if a tab ever legitimately carries both —
   today that never happens for `class` (it has no TAB_KINDS entry, so
   unreadCount('class') is always 0), but money/docs still take .badge and
   `class` still takes .gap on the very same bar, so getting the corner right
   is what keeps this from being one lucky coincidence away from a collision.
   `inset-inline-*` is safe to use here, unlike on an .msi glyph (see the
   .sec-toggle .chev note above): `.gap` is not a font ligature and sets no
   direction of its own, so these two properties resolve against the
   document's own RTL exactly as written — the trap that bit .chev twice
   does not apply to a plain <span>. */
.navitem .gap {
  position: absolute; bottom: -2px; inset-inline-start: 6px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--md-sys-color-error);
  /* The ring, in the bar's own surface colour rather than transparent, is
     what keeps the dot readable against the FILLED pill a selected tab draws
     behind its icon (.navitem[aria-selected="true"] .ind, above) — without
     it the dot's own edge disappears into a background close to it in value. */
  border: 2px solid var(--md-sys-color-surface);
  pointer-events: none;
}

/* ---------- dialog ---------- */
/* `margin: auto` is B-8, and it is restoring something rather than adding it.
   md-dialog's own shadow styles say `:host { margin: auto; position: fixed;
   inset: 0 }` -- that margin is what centres the dialog inside the inset box.
   The universal reset at the top of this file (`* { margin: 0; padding: 0 }`)
   matches custom elements like anything else, and a PAGE rule beats a shadow
   rule on the same element whatever the specificity says, so the centring was
   being cancelled here and the dialog resolved flush to the start corner --
   in rtl, top-right. Measured before the fix: 375px viewport, dialog 345.18
   wide at x=30.02 with all 30px of slack on one side, its right edge 0.2px
   off-screen, shaving the first glyph off every headline in the app.
   The inner <dialog> takes `margin: inherit` from the host, so restoring it
   here reaches both.
   B-8's OTHER half -- the reset also flattens Material's
   `slot[name="…"]::slotted(*)` paddings to 0 -- is deliberately not fixed
   here: every sheet in the app has been laid out against 0-padding dialogs
   for many versions, so putting 24px back is a per-sheet pass rather than a
   line of CSS. It is B-9. */
md-dialog { max-width: min(92vw, 420px); margin: auto; }
/* A glyph in a dialog headline. md-dialog's headline slot is a flex row with no
   gap of its own, so an icon placed in it touches the first letter.
   margin-inline-end here would be the same trap .sec-toggle .chev already hit:
   .msi sets direction:ltr on ITSELF, so its own logical end resolves against
   ltr (= physical right), not against the rtl row it sits in. Measured in the
   browser: margin-inline-end landed as margin-right, pushing the glyph further
   toward the dialog's right edge instead of opening a gap toward the text on
   its left. A plain margin-left sidesteps direction entirely. */
md-dialog [slot="headline"] .msi { margin-left: 8px; font-size: 22px; }

/* Document viewer: as much of the screen as a dialog can reasonably take,
   because a boarding pass read through a letterbox is no use. */
md-dialog.dlg-doc { max-width: min(96vw, 560px); }
.doc-view {
  display: block; width: 100%; border: 0; border-radius: var(--shape-s);
  background: var(--md-sys-color-surface-container-highest);
}
img.doc-view { max-height: 62vh; object-fit: contain; }
iframe.doc-view { height: 62vh; }
.dlg-actions { display: flex; gap: 8px; justify-content: flex-end; }

/* ---------- snackbar ---------- */
/* MD3 snackbar. Material Web has no snackbar component yet, so this follows the
   spec: inverse surface, 4dp corner, bottom-anchored above the nav bar. */
.snackbar {
  position: fixed; bottom: 96px; left: 50%; transform: translateX(-50%);
  z-index: 60; max-width: min(92vw, 420px);
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  border-radius: var(--shape-xs);
  padding: 14px 16px; font-size: 14px; line-height: 20px;
  box-shadow: 0 3px 8px rgba(0,0,0,.3);
  display: flex; align-items: center; gap: 10px;
}
.snackbar .msi { font-size: 20px; }
.snackbar.err { background: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); }

@media print {
  body > * { display: none !important; }
  #printArea { display: block !important; direction: rtl; font-family: sans-serif; }
  .pcard { border: 2px solid #000; border-radius: 10px; padding: 14px; margin: 0 0 10px; page-break-inside: avoid; }
  .pcard h3 { font-size: 22px; margin-bottom: 4px; }
  .pcard p { font-size: 14px; margin: 2px 0; }
}
#printArea { display: none; }
