/* ==========================================================================
   Expense Guru — design tokens (GENERATED)
   Source: packages/core/design-tokens.js
   Regenerate: node tools/design/generate-token-css.mjs
   Do not edit by hand. Every literal lives in :root; rules use var() only.
   ========================================================================== */

:root {
  /* ---- colour ----
   *
   * V2: the neutrals are tinted into the accent's own hue, deep is a second dark ground whose
   * job is to hold the accent as a small mark (lime on it measures 10.7:1), and the accent ramp
   * gained a softest step so a card can be washed with the accent without becoming the hero. */
  --canvas: #E9E7DA;
  --surface: #FCFDF9;
  --raised: #FFFFFF;
  --field: #E1E0D1;
  --hover: #F1EFE4;
  --dark: #14180F;
  --dark-hover: #242A1B;
  --deep: #20301A;
  --ink: #14180F;
  --ink-inv: #FFFFFF;
  --mute: #5E6553;
  --mute-dark: #ABAF9E;
  --faint: #7E8470;
  --hairline: rgba(20,24,15,.07);
  --border: rgba(20,24,15,.07);
  --lime-tint: #F3F9E4;
  --lime-100: #E9F4CB;
  --lime-300: #DAEE9E;
  --lime-500: #C8F04F;
  --grey-cell: #EBEEE3;
  --grey-bar: #DDE3D0;
  --danger: #B0443D;
  --danger-tint: #F7E7E5;
  --overlay: rgba(20,24,15,.44);

  /* ---- radius ---- */
  --r-xs: 8px;
  --r-sm: 12px;
  --r-md: 18px;
  --r-lg: 28px;
  --r-xl: 36px;
  --r-pill: 999px;

  /* ---- spacing ---- */
  --sp-2: 2px;
  --sp-4: 4px;
  --sp-6: 6px;
  --sp-8: 8px;
  --sp-10: 10px;
  --sp-12: 12px;
  --sp-14: 14px;
  --sp-16: 16px;
  --sp-20: 20px;
  --sp-24: 24px;
  --sp-28: 28px;
  --sp-32: 32px;
  --sp-40: 40px;
  --sp-48: 48px;
  --sp-56: 56px;
  --sp-64: 64px;
  --sp-80: 80px;

  /* ---- gaps, by relationship. See gapUsage for what each one means. ---- */
  --page-inset: 24px;
  --c-device-w: 430px;
  --c-device-h: 932px;
  --c-device-bezel: 12px;
  --c-device-r: 56px;
  --c-device-screen-r: 44px;
  --c-device-statusbar: 54px;
  --c-device-island-top: 11px;
  --c-device-homebar-bottom: 9px;
  --c-device-homebar: 34px;
  --c-device-island-w: 125px;
  --c-device-island-h: 36px;
  --c-device-homebar-w: 140px;
  --c-device-homebar-h: 5px;
  --g-inline: 6px;
  --g-tight: 10px;
  --g-control: 14px;
  --g-sibling: 20px;
  --g-section: 40px;
  --sp-0: 0;

  /* ---- motion ---- */
  --ease: cubic-bezier(.2,.7,.2,1);
  --spring: cubic-bezier(.34,1.3,.4,1);
  --t-fast: .18s;
  --t-med: .5s;
  --t-slow: .8s;

  /* ---- type ---- */
  --font: 'Hanken Grotesk', system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-icon: 'Phosphor';
  /* THE MONEY FACE, AND THE SIZE BELOW WHICH IT IS NOT USED.
   *
   * Both are emitted so the rule travels with the face. --font-money-min is not decoration: it is
   * the measured floor below which the dot-matrix glyphs stop resolving, and a rule that applies
   * the money face must respect it. A screen that wants the face at 16px is asking for money nobody
   * can read, and the token is what lets that be checked rather than argued about. */
  --font-money: 'Matrix Sans Print Tabular';
  --font-money-min: 38px;
  --fs-display: clamp(32px, 4.4vw, 42px);
  --fs-numxl: clamp(38px, 5.2vw, 54px);
  --fs-figure: clamp(52px, 9vw, 76px);
  --fs-title: 20px;
  --fs-body: 16px;
  --fs-label: 14px;
  --fs-cap: 13px;
  --fs-tiny: 12.5px;
  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  /* V2. The scale had four weights in the token source and emitted three, so a screen that asked
     for bold got a var() that resolved to nothing and the browser fell back to the weight it would
     have used anyway. The hero figure is the reason it matters now: it is set at 700. */
  --fw-bold: 700;

  /* ---- component measurements ---- */
  /* chat: bubble, composer, mention. These were missing, so every .eg-bubble and
     .eg-composer rule referenced a variable that did not exist and rendered as
     nothing - the money bubbles came out transparent. */
  --c-bubble-py: 14px;
  --c-bubble-px: 18px;
  --c-bubble-r: 20px;
  --c-bubble-gap: 16px;
  --c-bubble-max: 78%;
  --c-bubble-bg: #E1E0D1;
  --c-bubble-fg: #14180F;
  --c-bubble-money-bg: #20301A;
  --c-bubble-money-fg: #FFFFFF;
  --c-composer-h: 56px;
  --c-composer-r: 28px;
  --c-composer-py: 10px;
  --c-composer-px: 18px;
  --c-composer-bg: #FFFFFF;
  --c-composer-border: rgba(20,24,15,.07);
  --c-composer-gap: 10px;
  --c-mention-r: 999px;
  --c-mention-py: 1px;
  --c-mention-px: 6px;
  --c-mention-bg: #E9F4CB;
  --c-mention-fg: #14180F;
  --c-mention-weight: 600;
  --c-split-py: 18px;
  --c-split-px: 18px;
  --c-split-gap: 12px;
  --c-split-r: 18px;
  --c-split-row-gap: 8px;
  --c-split-bg: #FCFDF9;
  --c-split-border: #C8F04F;
  --c-split-amount: clamp(38px, 5.2vw, 54px);
  --c-pop-r: 18px;
  --c-switch-h: 34px;
  --c-switch-pad: 3px;
  --c-switch-gap: 2px;
  --c-switch-bg: rgba(255,255,255,.12);
  --c-switch-fg: #ABAF9E;
  --c-switch-on-bg: #C8F04F;
  --c-switch-on-fg: #14180F;
  --c-switch-px: 12px;
  --c-switch-p-r: 999px;
  --c-switch-fs: 12.5px;
  /* THE HERO SURFACE. Emitted because the app is what has to paint the hero, and a token the
     stylesheet never emits resolves to nothing - the app would have fallen back to its own
     hardcoded gradient and the two would drift apart silently. */
  /* THE ENTRANCE LADDER, as durations. A delay is a motion decision, so it lives with motion. */
  --d-home-hero: 0ms;
  --d-home-pace: 60ms;
  --d-home-stat1: 120ms;
  --d-home-stat2: 160ms;
  --d-home-stat3: 200ms;
  --d-home-stat4: 240ms;
  --d-home-cats: 220ms;
  --d-home-latest: 320ms;
  /* THE GLYPH SCALE. An icon size is a design decision, so it is a token - the app carried five
     raw pixel counts (15/16/18/20/22) with nothing behind any of them. */
  --i-sm: 14px;
  --i-md: 16px;
  --i-lg: 20px;
  --i-xl: 22px;
  --c-hero-floor: #0E1209;
  --c-hero-floor-gradient: linear-gradient(150deg, var(--deep) 0%, var(--dark) 58%, var(--c-hero-floor) 100%);
  --c-hero-ground: var(--c-hero-bloom), var(--c-hero-floor-gradient);
  --c-hero-bloom: radial-gradient(120% 140% at 12% 0%, rgba(200,240,79,.16) 0%, rgba(200,240,79,0) 55%);
  --c-hero-fg: #FFFFFF;
  --c-hero-maxw: 760px;
  --c-hero-pad: 28px 24px 24px;
  --c-hero-r: 36px;
  --c-hero-grid-cell: 34px;
  --c-hero-grid-line: rgba(255,255,255,.07);
  --c-hero-grid-fade: radial-gradient(100% 90% at 70% 30%, #000 0%, rgba(0,0,0,.5) 55%, transparent 92%);
  --c-hero-ctl-bg: rgba(255,255,255,.12);
  --c-hero-ctl-bg-hover: rgba(255,255,255,.2);
  --c-hero-ctl-bg-edit: rgba(255,255,255,.22);
  /* THE FILL SYSTEM. A control's fill against its ground may be quiet (>= 1.15:1); what must not be
     quiet is its CONTENT, which is why every fill carries the ink it is legal with. */
  --f-brand-bg: #20301A;
  --f-brand-fg: #FFFFFF;
  --f-accent-bg: #C8F04F;
  --f-accent-fg: #14180F;
  --f-accent-soft-bg: #E9F4CB;
  --f-accent-wash-bg: #F3F9E4;
  --f-quiet-bg: #FFFFFF;
  --f-quiet-fg: #14180F;
  --f-inset-bg: #E1E0D1;
  --c-pop-pad: 14px 16px;
  --c-pop-pad-y: 14px;
  --c-pop-gap: 10px;
  --c-pop-bg: #FFFFFF;
  --c-pop-border: 1px solid rgba(20,24,15,.07);
  --c-pop-shadow: 0 10px 30px rgba(20,24,15,.16);
  --c-pop-maxw: calc(100vw - 48px);
  --c-pop-w: 260px;
  --c-suggest-r: 12px;
  --c-suggest-row-py: 10px;
  --c-suggest-row-px: 12px;
  --c-suggest-gap: 10px;
  --c-suggest-bg: #FFFFFF;
  --c-suggest-border: rgba(20,24,15,.07);
  --c-suggest-on: #E1E0D1;
  --c-badge-minw: 18px;
  --c-badge-h: 18px;
  --c-badge-px: 5px;
  --c-badge-fs: 12.5px;
  --c-badge-fw: 700;
  --c-badge-bg: #C8F04F;
  --c-badge-fg: #14180F;
  --c-badge-ring-w: 2px;
  --c-roomtabs-r: 999px;
  --c-roomtabs-p: 6px;
  --c-roomtabs-gap: 6px;
  --c-roomtabs-inset: 24px;
  --c-roomtabs-tab-py: 14px;
  --c-roomtabs-tab-px: 14px;
  --c-roomtabs-bg: #E1E0D1;
  --c-roomtabs-fg: #5E6553;
  --c-roomtabs-on-bg: #14180F;
  --c-roomtabs-on-fg: #FFFFFF;
  --c-head-pt: 16px;
  --c-head-pb: 12px;
  --c-head-row: var(--c-touch);
  --c-head-h: calc(var(--c-head-pt) + var(--c-head-row) + var(--c-head-pb));
  --c-head-x: 10px;
  --c-head-gap: 14px;
  --c-pill-h: 48px;
  --c-pill-h-sm: 36px;
  --c-pill-px: 22px;
  --c-pill-px-sm: 16px;
  --c-circle-dark-hover: #242A1B;
  --c-chart-on: #C8F04F;
  --c-chart-rest-dark: rgba(255,255,255,.18);
  --c-chart-on-dark: #C8F04F;
  --c-chart-h: 120px;
  --c-chart-r: 18px;
  --c-chart-rest: #DDE3D0;
  --c-chart-min: 5%;
  --c-chart-bubble-gap: 12px;
  --c-chart-shift: 0px;
  /* The reading's horizontal correction, as its own property because the app writes it per
     bubble. Declared here so the base value is a token rather than a literal repeated in the
     fallback, and so the property EXISTS before React writes one. */
  --shift: 0px;
  --c-chart-dot: 10px;
  --c-chart-room: calc(var(--c-pop-pad-y) * 2 + var(--fs-label) * 1.3 + var(--c-chart-bubble-gap));
  --c-chart-hatch: repeating-linear-gradient(45deg, #C8F04F 0px, #C8F04F 3px, transparent 3px, transparent 6px);
  --c-chart-hatch-on: repeating-linear-gradient(45deg, #14180F 0px, #14180F 3px, transparent 3px, transparent 6px);
  /* ONE SOURCE FOR THE PAIR. The circle's steps ARE the icon-surface steps - the same physical
     thing, declared once - so a circle cannot be a size the system does not know. */
  --c-circle-size: 40px;
  --c-circle-size-chrome: 48px;
  --c-circle-glyph-chrome: 24px;
  --c-circle-size-action: 40px;
  --c-circle-size-sm: 36px;
  --c-circle-glyph: 20px;
  --c-circle-glyph-action: 20px;
  --c-circle-glyph-sm: 18px;
  --c-ico-size: 48px;
  --c-ico-size-sm: 36px;
  --c-ico-size-lg: 56px;
  --c-ico-glyph: 24px;
  --c-ico-glyph-sm: 18px;
  --c-ico-glyph-lg: 28px;
  --c-overlay-scrim: rgba(23,24,26,.38);
  --c-overlay-z: 60;
  --c-overlay-pad: 16px;
  --c-overlay-in: 200ms;
  --c-modal-bg: #FCFDF9;
  --c-modal-radius: 28px;
  --c-modal-maxw: 480px;
  --c-modal-maxh: 90dvh;
  --c-modal-pad: 24px;
  --c-modal-in: 280ms;
  --c-modal-rise: 8px;
  --c-modal-from: .94;
  --c-cal-cell: 40px;
  --c-cal-rowgap: 2px;
  --c-cal-month: 15px;
  --c-cal-month-ink: #14180F;
  --c-cal-wd-size: 11px;
  --c-cal-wd-ink: #5E6553;
  --c-cal-day-ink: #14180F;
  --c-cal-today-ring: #14180F;
  --c-cal-on-fill: #14180F;
  --c-cal-on-ink: #FFFFFF;
  --c-cal-out-ink: #5E6553;
  --c-cal-off-ink: #5E6553;
  --c-time-dial: 232px;
  --c-time-num: 44px;
  --c-time-r: calc((var(--c-time-dial) - var(--c-time-num)) / 2);
  --c-time-num-ink: #14180F;
  --c-time-on-fill: #14180F;
  --c-time-on-ink: #FFFFFF;
  --c-time-mid: 13px;
  --c-time-mer-fill: #E1E0D1;
  --c-time-mer-on: #14180F;
  --c-time-mer-on-ink: #FFFFFF;
  --c-time-min-fill: #FFFFFF;
  --c-time-min-ink: #14180F;
  --c-time-min-on: #14180F;
  --c-time-min-on-ink: #FFFFFF;
  --c-tag-py: 8px;
  --c-tag-px: 14px;
  --c-tag-fs: 13px;
  --c-tag-hero-fs: 15px;
  --c-tag-ol-fill: #E9F4CB;
  --c-tag-ol-stroke: #C8F04F;
  --c-tag-ol-gap: 6px;
  --c-tag-ol-icon: 15px;
  --c-tag-ol-fs: 12.5px;
  --c-tag-hero-py: 10px;
  --c-tag-hero-px: 16px;
  --c-dot-size: 8px;
  --c-dot-ring: 3px;
  --c-input-h: 52px;
  --c-input-px: 18px;
  --c-input-border: 1.5px solid rgba(20,24,15,.07);
  --c-row-py: 16px;
  --c-row-px: 10px;
  --c-row-gap: 16px;
  --c-focus-w: 3px;
  --c-focus-offset: 2px;
  /* THE GLYPH SCALE, AS ONE SET OF SIZES. This was 18px written straight into the emitter - a number
     with no token behind it, and a SECOND scale beside the one in the token source (14/16/20/22).
     The app read one and the screens read the other, so the same glyph could be 18px in a screen
     and 16px in the app. The icon-size variable is the default step and keeps its name, because
     four rules already read it. */
  --c-icon-size: 16px;
  --c-card-bg: #FFFFFF;
  --c-card-r: 28px;
  --c-card-padding: 28px;
  --c-row-neg-x: -10px;
  --c-touch: 44px;
  --c-touch-min: 24px;
  --c-narrow: 420px;
  --c-narrow-wide: 760px;
  --c-step-size: 10px;
  --c-step-gap: 8px;
  --c-avatar-size: 40px;
  --c-avatar-size-lg: 72px;
  --c-avatar-fs-lg: clamp(32px, 4.4vw, 42px);
  --c-tabbar-inset: 24px;
  --c-tabbar-r: 999px;
  --c-tabbar-pad: 8px;
  --c-tabbar-gap: 6px;
  --c-tabbar-px: 22px;
  --c-tabbar-bg: #14180F;
  --c-tabbar-border: none;
  --c-tabbar-fg: #ABAF9E;
  --c-tabbar-on-bg: #C8F04F;
  --c-tabbar-on-fg: #14180F;
  --c-tabbar-on-gap: 9px;
  --c-tabbar-hover-fg: #FFFFFF;
  --c-tabbar-label-max: 120px;
  --c-tabbar-badge-x: 8px;
  --c-tabbar-badge-y: -7px;
  --c-tabbar-py: 14px;
  /* The space the fixed bar occupies, so a full-bleed screen can reserve it. */
  --c-tabbar-reserve: calc(var(--c-tabbar-py) * 2 + var(--c-tabbar-icon) + var(--c-tabbar-pad) * 2 + var(--sp-32));
  --c-tabbar-icon: 24px;
  --c-tabbar-label: 13px;
  --c-badge-ring: 2px;
  --c-claim-pad: 16px;
  --c-empty-py: 40px;
  --c-skel-h: 12px;
  --c-skel-gap: 10px;
  --c-fuel-h: 16px;
  --c-fuel-track: #20301A;
  --c-fuel-tick: 3px;
  --c-safe-top: env(safe-area-inset-top, 0px);
  --c-safe-top-min: 54px;
  --c-safe-bottom: env(safe-area-inset-bottom, 0px);
  --c-safe-bottom-min: 16px;
  --bp-xs: 400px;
  --bp-sm: 480px;
  --bp-md: 560px;
  --bp-lg: 640px;
  --bp-xl: 720px;
  --bp-2xl: 900px;
  --bp-3xl: 1024px;
  --bp-4xl: 1180px;
  /* A RING is its own measurement. A hairline divides two areas INSIDE one surface; a focus outline
     is a state. A ring separates two OVERLAPPING surfaces - the avatar stack - and was a bare 2px
     in one screen until it was named. */
  --c-ring-w: 2px;
  --c-card-padding-emphasis: 20px;
  --c-card-pad-list: 14px;
  --c-card-pad-card: 16px;
  --c-card-pad-feature: 24px;
  --c-hairline-w: 1px;
  --c-swipe-w: 88px;
  --c-label-min: 140px;
  --c-bell-size: 48px;
  --c-bell-icon: 24px;
  --c-bell-bg: #FCFDF9;
  --c-bell-border: 1px solid rgba(20,24,15,.07);
  --c-bell-badge-bg: #14180F;
  --c-bell-badge-fg: #C8F04F;
  --c-bell-badge-minw: 20px;
  --c-bell-badge-h: 20px;
  --c-bell-badge-px: 6px;
  --c-bell-badge-fw: 600;
  --c-bell-badge-ring: 2px;
  --c-bell-badge-ringc: #FCFDF9;
  --c-toast-pad: 14px 16px 14px 22px;
  --c-toast-max: calc(100vw - 32px);
  --c-toast-ring-w: 3px;
  --t-toast: 5000ms;
  --c-choice-h: 48px;
  --c-choice-px: 20px;
}

/* ------------------------------------------------------------------ reset */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: var(--sp-0); padding: var(--sp-0); background: var(--canvas); color: var(--ink); }
body { font-family: var(--font); font-size: var(--fs-body); -webkit-font-smoothing: antialiased; }

/* ------------------------------------------------------------ typography */
.eg-display { font-size: var(--fs-display); font-weight: var(--fw-regular); line-height: 1.08; letter-spacing: -0.01em; margin: var(--sp-0); }
.eg-numxl { font-size: var(--fs-numxl); font-weight: var(--fw-semibold); line-height: 1; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
.eg-num { font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.eg-title { font-size: var(--fs-title); font-weight: var(--fw-semibold); letter-spacing: -0.005em; margin: var(--sp-0); }
.eg-body { font-size: var(--fs-body); font-weight: var(--fw-medium); line-height: 1.5; }
.eg-label { font-size: var(--fs-label); font-weight: var(--fw-medium); }
.eg-cap { font-size: var(--fs-cap); font-weight: var(--fw-regular); line-height: 1.5; }
.eg-tiny { font-size: var(--fs-tiny); font-weight: var(--fw-regular); }
.eg-mute { color: var(--mute); }
.eg-faint { color: var(--faint); }
.eg-danger { color: var(--danger); }

/* --------------------------------------------------------------- surfaces */
/* The card fill comes from the token, so a card is white everywhere by one decision. */
.eg-card { background: var(--c-card-bg); border-radius: var(--c-card-r); }
.eg-card--dark { background: var(--dark); color: var(--ink-inv); }
.eg-hairline { border-bottom: 1px solid var(--hairline); }

/* ------------------------------------------------------------------ pills */
/* --------------------------------------------------------------- figures */
/* THE ONE BIG NUMBER ON A SCREEN, in the money face at the figure step.
   This was a rule inside the app's own stylesheet, so the money face
   reached exactly one screen - the home hero - and every other screen's headline number rendered in
   the text face. A design decision that lives in one screen is not a design system. */
.eg-figure {
  font-family: var(--font-money);
  font-size: var(--fs-figure);
  font-weight: var(--fw-regular);
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
/* A figure that is smaller than the hero's but still a figure - a per-room total, a settled amount.
   Kept as its own step because the dot-matrix face needs room for its dots to resolve: below 38px
   the money-font gate refuses it, so this is the next step up from body type, not below it. */
.eg-figure--sm { font-size: var(--fs-title); }

/* ---------------------------------------------------------------- fills */
/* A control's fill, named by its job. The pair always travels together, because a fill is only
   legal with the ink it was measured against - and separating them is how a white label ends up on
   lime at 1.31:1. */
.eg-fill--brand { background: var(--f-brand-bg); color: var(--f-brand-fg); }
.eg-fill--accent { background: var(--f-accent-bg); color: var(--f-accent-fg); }
.eg-fill--accent-soft { background: var(--f-accent-soft-bg); color: var(--f-accent-fg); }
.eg-fill--accent-wash { background: var(--f-accent-wash-bg); color: var(--f-accent-fg); }
.eg-fill--quiet { background: var(--f-quiet-bg); color: var(--f-quiet-fg); }
.eg-fill--inset { background: var(--f-inset-bg); color: var(--f-quiet-fg); }

/* THE FILL SYSTEM, WHICH IS WHY THIS LIVES HERE AND NOT IN A SCREEN.
   The primitive says WHICH JOB a control does; the fill token says which colour that is. Putting
   this in the app instead would have reached only the app's own screens and left all twenty-one
   GENERATED screens reading the old styling - which is exactly what happened before it was moved. */
.eg-pill {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--g-inline);
  background: var(--f-quiet-bg); color: var(--f-quiet-fg); border: none; border-radius: var(--r-pill);
  font: inherit; font-size: var(--fs-label); font-weight: var(--fw-medium);
  height: var(--c-pill-h); padding: var(--sp-0) var(--c-pill-px); cursor: pointer;
  white-space: nowrap; flex: none;
  transition: transform var(--t-fast) ease, background var(--t-fast) ease, opacity var(--t-fast) ease;
}
.eg-pill:hover { transform: translateY(-1px); }
.eg-pill:active { transform: scale(.97); }
/* The emphasis fill. Its own fill IS its boundary, so it needs no edge. */
.eg-pill--cta { background: var(--f-brand-bg); color: var(--f-brand-fg); }
.eg-pill--cta:hover { background: var(--dark-hover); }
/* A POSITIVE CONFIRMATION - "Received", "Review it". Not the screen's primary action, so it must
   not wear the primary fill: three dark buttons on one screen leave nothing reading as THE action.
   The accent says which answer is the good one, at 13.73:1 for the ink on it. */
.eg-pill--accent { background: var(--f-accent-bg); color: var(--f-accent-fg); }
/* A SECONDARY ACTION ON A CARD. On a white card only three fills clear the 1.15:1 floor - brand at
   13.71:1, inset at 1.30:1 and accent at 1.28:1. A plain white pill measures 1.02:1 and a soft
   accent tint 1.13:1, which is the merge this fixes. */
.eg-pill--on-card { background: var(--f-accent-bg); color: var(--f-accent-fg); }
.eg-pill--sm { height: var(--c-pill-h-sm); padding: var(--sp-0) var(--c-pill-px-sm); font-size: var(--fs-cap); }
.eg-pill--block { width: 100%; }
.eg-pill--danger { background: var(--raised); color: var(--danger); }
.eg-pill[disabled], .eg-pill[aria-disabled="true"] { opacity: .4; cursor: not-allowed; transform: none; }
.eg-pill[disabled]:hover, .eg-pill[aria-disabled="true"]:hover { transform: none; }

/* ----------------------------------------------------------------- circles */
.eg-circle {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: var(--c-circle-size); height: var(--c-circle-size); border-radius: var(--r-pill);
  /* THE PAIR CANNOT DRIFT: the glyph is half the circle and derives from it, so a circle can never
     be one size with a glyph from another step. Declared as one rule rather than as two tokens
     somebody has to keep in step. */
  font-size: var(--c-circle-size);
  background: var(--f-quiet-bg); color: var(--f-quiet-fg); border: none; font: inherit; cursor: pointer;
  transition: transform var(--t-fast) ease, background var(--t-fast) ease;
}
/* Hover lifts, and does not repaint. Setting a light background here made the
   DARK circle a white icon on a light surface - 1.00:1, invisible on hover. The
   base already has its own background, so there is nothing to override. */
.eg-circle:hover { transform: translateY(-1px); }
.eg-circle:active { transform: scale(.94); }
/* THE GLYPH IS HALF THE CIRCLE, as an explicit declared size rather than a derived one. The em form
   was tidier but resolved against a smaller inherited base than the circle's own size, and produced
   8px glyphs where 20px was intended - a pair that is DECLARED can be checked, and one that is
   derived through the cascade has to be measured to be believed. */

.eg-circle--dark { background: var(--f-brand-bg); color: var(--f-brand-fg); }
.eg-circle--well { background: var(--f-inset-bg); color: var(--f-quiet-fg); }
.eg-circle--sm { width: var(--c-circle-size-sm); height: var(--c-circle-size-sm); }
/* The chrome step: a control in the screen's own frame. */
.eg-circle--chrome { width: var(--c-circle-size-chrome); height: var(--c-circle-size-chrome); }

/* THE CIRCLE AN ICON SITS IN WHEN IT IS NOT A CONTROL.
   Same shape and ground as the circle above, minus everything that only means
   something on a button: no pointer, no lift, no press. Four screens used to
   answer this separately - two drew a grey well, one a white circle, one lime -
   so the same idea looked like four different things. One ground now, which is
   raised (pure white), plus the three sizes and the named variants. */
.eg-ico {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: var(--c-ico-size); height: var(--c-ico-size); border-radius: var(--r-pill);
  background: var(--raised); color: var(--ink);
}
.eg-ico--sm { width: var(--c-ico-size-sm); height: var(--c-ico-size-sm); }
.eg-ico--lg { width: var(--c-ico-size-lg); height: var(--c-ico-size-lg); }
.eg-ico--well { background: var(--field); }
.eg-ico--accent { background: var(--lime-500); }
.eg-ico--dark { background: var(--dark); color: var(--ink-inv); }
.eg-ico--danger { background: var(--raised); color: var(--danger); }
/* A glyph in a surface is sized to the surface, not to the page: the circle is
   40px and the icon is 22px, so it reads as a mark rather than a hole. */
.eg-ico .eg-icon { font-size: var(--c-ico-glyph); }
.eg-ico--sm .eg-icon { font-size: var(--c-ico-glyph-sm); }
.eg-ico--lg .eg-icon { font-size: var(--c-ico-glyph-lg); }

/* -------------------------------------------------------------- time dial
   A TIME IS NOT A DATE. A calendar is the wrong control for "9:00 AM", which is why that was a
   text field for so long and why this is its own component rather than a mode of the calendar.

   The twelve hours sit on a circle, the way every phone clock has shown them for forty years.
   Each number is placed by one angle and rotated back by the same angle so it stays upright, and
   the angle comes from the hour - so 7 can never land where 8 goes. */
/* THE DIAL SHRINKS, AND ITS GEOMETRY FOLLOWS.
 *
 * The hours are placed by a radius derived from the dial size, so a dial that shrinks while the
 * radius does not would throw the numbers off the circle. Both read the same variable here, so
 * capping the dial caps the radius with it and the twelve hours stay on it at any width. Without
 * the cap the dial is a fixed 232px, which is wider than a 280px screen can give it and pushes the
 * page sideways. */
.eg-time {
  display: flex; flex-direction: column; align-items: center; gap: var(--g-sibling);
  width: 100%; max-width: var(--c-time-dial); min-width: 0;
}
.eg-time__dial {
  position: relative; flex: none;
  /* THE DIAL IS MEASURED AGAINST ITS OWN BOX, NOT AGAINST A PERCENTAGE.
   *
   * This was min(<dial>, 100%). A percentage width resolves against the CONTAINING BLOCK's width,
   * and the dial's containing block is the centred flex column, so on a page where that column has
   * no definite width the percentage resolves against nothing and the dial came out 232x0 - a
   * zero-height circle with the twelve hours stacked on top of each other. The parent above now
   * claims the full width up to the dial's own size, which gives the percentage something real to
   * resolve against at every viewport, and the radius below follows the resolved value so the
   * hours stay on the circle at any width. */
  width: min(var(--c-time-dial), 100%); height: min(var(--c-time-dial), 100%);
  aspect-ratio: 1;
  border-radius: var(--r-pill); background: var(--field);
}
.eg-time__num {
  position: absolute; left: 50%; top: 50%;
  display: flex; align-items: center; justify-content: center;
  width: var(--c-time-num); height: var(--c-time-num);
  margin: calc(var(--c-time-num) / -2) var(--sp-0) var(--sp-0) calc(var(--c-time-num) / -2);
  border: 0; border-radius: var(--r-pill); background: none;
  color: var(--c-time-num-ink); font: inherit; font-size: var(--fs-label);
  font-variant-numeric: tabular-nums; cursor: pointer;
  transform: rotate(var(--a)) translateY(calc(var(--c-time-r) * -1)) rotate(calc(var(--a) * -1));
}
.eg-time__num:hover { background: var(--hover); }
.eg-time__num--on, .eg-time__num--on:hover { background: var(--c-time-on-fill); color: var(--c-time-on-ink); }
/* The middle answers "9 what?" without a second control to go and find. */
.eg-time__mid {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: var(--g-inline);
  pointer-events: none;
}
.eg-time__read { font-size: var(--fs-title); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.eg-time__mer {
  pointer-events: auto;
  min-width: var(--c-touch); min-height: var(--c-touch);
  border: 0; border-radius: var(--r-pill); background: var(--c-time-mer-fill);
  color: var(--c-time-mid); font: inherit; font-size: var(--fs-cap); font-weight: var(--fw-semibold);
  cursor: pointer;
}
.eg-time__mer[data-meridiem="PM"] { background: var(--c-time-mer-on); color: var(--c-time-mer-on-ink); }
.eg-time__mins { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--g-inline); }
/* THE MINUTE PILLS DO NOT SHRINK BELOW THE FLOOR.
 *
 * They ask for a 44px width and height, and all twelve sit in a wrapping row. When the row is
 * narrower than twelve of those, the pills are squeezed: as flex items they shrink, and they shrink
 * straight past the 24px that WCAG 2.5.8 allows, down to 22px on a 414px screen. min-width:auto
 * does not save them, because their own min-width is smaller than the floor. So the floor is a
 * FLOOR - the declared size is what they want, 24 is what they may never go under, and the row
 * wraps instead of squeezing. */
.eg-time__min {
  /* THE PILL DOES NOT SQUEEZE. Twelve of these at their declared 44px do not fit one row, so the
     row wraps - but as shrinkable flex items they were being squeezed instead, down to 30px wide,
     which is below the 24px floor once the pill's own rounding is taken into account and which axe
     reports as a target-size failure. flex:none means they keep the size they declare and the row
     wraps to a second line, which is what a wrapping row is for. */
  flex: none;
  min-width: var(--c-touch); min-height: var(--c-touch);
  border: var(--c-hairline-w) solid var(--border); border-radius: var(--r-pill);
  background: var(--c-time-min-fill); color: var(--c-time-min-ink);
  font: inherit; font-size: var(--fs-label); font-variant-numeric: tabular-nums; cursor: pointer;
}
.eg-time__min--on { background: var(--c-time-min-on); color: var(--c-time-min-on-ink); border-color: var(--c-time-min-on); }

/* ------------------------------------------------------------ overlay + modal
   THE CALENDAR OPENS IN ONE OF THESE, and so does anything else that must not push the page
   around: a picker that shoves the form down makes the person lose their place. This is the
   overlay the Home page already used for "What did the money do?", moved here from the app's own
   stylesheet so a designed screen can reach it too.

   Tapping the scrim closes. The dialog stops the click, so a tap inside it is not a dismissal. */
.eg-overlay {
  position: fixed; inset: 0; z-index: var(--c-overlay-z);
  display: flex; align-items: center; justify-content: center;
  background: var(--c-overlay-scrim); padding: var(--c-overlay-pad);
}
/* The scrim is its own layer, and it is the thing that closes on a tap. Putting the close on the
   overlay itself meant a tap on a calendar day bubbled up and dismissed the dialog, because
   static markup has no way to stop a click. */
.eg-overlay__scrim { position: absolute; inset: 0; }
.eg-modal {
  position: relative; z-index: 1;
  width: 100%; max-width: var(--c-modal-maxw); max-height: var(--c-modal-maxh);
  overflow-y: auto; overscroll-behavior: contain;
  background: var(--c-modal-bg); border-radius: var(--c-modal-radius); padding: var(--c-modal-pad);
}
/* THE ENTRY ANIMATION BELONGS TO THE OPENING, NOT TO THE ELEMENT EXISTING.

   A modal that is already open when the page loads - which is exactly how a states harness shows
   one - must not fade in. It is simply there. With the animation on the base rule it painted at
   opacity 0 for its first 280ms, and anything measuring the page in that window concluded the
   dialog was invisible: the keyboard audit fell back to checking the whole document and reported
   53 unreachable controls on a dialog that was fine.

   The app adds --in when it opens one, so the animation still plays where it means something. */
.eg-overlay--in { animation: egFade var(--c-overlay-in) ease; }
.eg-overlay--in .eg-modal { animation: egPop var(--c-modal-in) var(--spring); }
/* iOS keeps vh at the full screen height with the keyboard up, which pushes the action row out of
   reach. dvh tracks the visible viewport instead. */
@supports (max-height: 90dvh) { .eg-modal { max-height: 90dvh; } }
.eg-modal__head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--g-control);
  margin-bottom: var(--g-sibling);
}
@keyframes egFade { from { opacity: 0 } to { opacity: 1 } }
@keyframes egPop { from { opacity: 0; transform: scale(var(--c-modal-from)) translateY(var(--c-modal-rise)) } to { opacity: 1; transform: scale(1) translateY(0) } }
@media (prefers-reduced-motion: reduce) {
  .eg-overlay, .eg-modal { animation: none; }
}

/* --------------------------------------------------------- date and calendar
   A DATE IS CHOSEN, NEVER TYPED. Both date fields in the product were text inputs holding
   "7 Oct 2026" and "9:00 AM" - a phone keyboard, an invented format, and no way to see that the
   7th is a Tuesday.

   The field is a BUTTON that shows the chosen date, so there is no format to get wrong and no
   invalid date to reject. It is deliberately shaped like .eg-input, because it sits in the same
   form column as the other fields and has to line up with them. */
.eg-datefield {
  display: flex; align-items: center; justify-content: space-between; gap: var(--g-inline);
  width: 100%; min-height: var(--c-touch);
  background: var(--raised); color: var(--ink);
  border: var(--c-hairline-w) solid var(--border); border-radius: var(--r-md);
  padding: var(--sp-0) var(--c-input-px);
  font: inherit; text-align: start; cursor: pointer;
}
.eg-datefield:hover { border-color: var(--faint); }
.eg-datefield[aria-expanded="true"] { border-color: var(--ink); }
.eg-datefield__value { flex: 1; min-width: var(--sp-0); }
/* An unchosen date says so rather than showing today's and pretending it was chosen. */
.eg-datefield--empty .eg-datefield__value { color: var(--mute); }

/* A CALENDAR SHRINKS. The month name and two 44px nav buttons are about 200px of content, and the
   calendar has to keep fitting inside whatever column it is given - in the app that column is a
   dialog, and on a 280px screen it is most of the display.
 *
 * This is the min-width:auto trap, and it is worth naming because it is invisible until the
 * container gets narrow. A flex or grid ITEM defaults to min-width:auto, which means it refuses to
 * shrink below its content. So the calendar reported a content width of 290px inside a 230px box
 * and pushed the whole page sideways, while every element inside it looked correctly sized.
 * max-width:100% bounds it, min-width:0 lets it actually shrink, and the header is allowed to wrap
 * so the month name drops below the buttons rather than forcing them off the edge. */
.eg-cal {
  display: flex; flex-direction: column; gap: var(--g-tight);
  /* A CALENDAR IS A CARD: it is a filled, rounded, padded container sitting on the canvas, the same
     object as every other card. It wore a well's radius and a control's padding, and a bare colour
     token for its fill - three primitives where the card step exists to answer all three. */
  background: var(--c-card-bg); border-radius: var(--c-card-r); padding: var(--c-card-padding);
  width: 100%; max-width: 100%; min-width: 0;
}
.eg-cal__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--g-inline); flex-wrap: wrap; min-width: 0;
}
.eg-cal__grid { display: flex; flex-direction: column; gap: var(--g-tight); min-width: 0; }
.eg-cal__month { font-size: var(--c-cal-month); font-weight: var(--fw-semibold); color: var(--c-cal-month-ink); min-width: 0; }
.eg-cal__nav {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: var(--c-touch); height: var(--c-touch); border: 0; border-radius: var(--r-pill);
  background: none; color: var(--ink); font: inherit; cursor: pointer;
}
.eg-cal__nav:hover { background: var(--hover); }
.eg-cal__nav .eg-icon { font-size: var(--c-icon-size); }

/* Seven columns, and a day is the touch target. The weekday letters are column headers, not
   decoration: a screen reader reads them, so they carry the full name in an abbr. */
/* minmax(0, 1fr), not 1fr. A 1fr track has an automatic minimum, so it will not go below the
   widest thing in it - here a two-digit day at the label size. Seven of those plus the gaps is
   wider than a 280px screen can give, so the row pushed the page sideways instead of shrinking. */
.eg-cal__week, .eg-cal__row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--c-cal-rowgap); min-width: 0; }
.eg-cal__wd {
  display: flex; align-items: center; justify-content: center;
  font-size: var(--c-cal-wd-size); font-weight: var(--fw-medium); color: var(--c-cal-wd-ink);
  text-transform: uppercase; letter-spacing: 0.04em;
}
/* A DAY IS A TARGET, AND 24px IS THE FLOOR IT MAY NOT GO BELOW.
 *
 * The cell is a square that fills its column, which is right while there is room. Once the columns
 * can shrink - see the minmax(0,1fr) note above - they eventually take the cell under the WCAG
 * 2.5.8 minimum: at 414px the seven columns gave 22px, and the audit reported it. So the square has
 * a floor. Below that width the cell is still the full width of its column and 24 tall, which is a
 * slightly tall target rather than an illegal one - the right way round. */
.eg-cal__day {
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 1; width: 100%; min-height: var(--c-touch-min);
  border: 0; border-radius: var(--r-pill);
  background: none; color: var(--c-cal-day-ink);
  font: inherit; font-size: var(--fs-label); font-variant-numeric: tabular-nums; cursor: pointer;
}
.eg-cal__day:hover { background: var(--hover); }
/* Today wears a RING, not a fill. Filled means chosen, and today is not a choice. */
.eg-cal__day--today { box-shadow: inset 0 0 0 var(--c-hairline-w) var(--c-cal-today-ring); }
.eg-cal__day--on, .eg-cal__day--on:hover { background: var(--c-cal-on-fill); color: var(--c-cal-on-ink); }
/* Days from the neighbouring month are shown so the grid is never ragged, and dimmed because
   they belong to a month the person is not looking at. */
.eg-cal__day--out { color: var(--c-cal-out-ink); }
.eg-cal__day[disabled] { color: var(--c-cal-off-ink); cursor: not-allowed; background: none; }
@font-face {
  font-family: 'Hanken Grotesk'; /* ds-allow-hardcode */
  src: url('./fonts/HankenGrotesk-latin.woff2') format('woff2');
  font-weight: 400 700; font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Hanken Grotesk'; /* ds-allow-hardcode */
  src: url('./fonts/HankenGrotesk-latin-ext.woff2') format('woff2');
  font-weight: 400 700; font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Icon glyph.
   PHOSPHOR, SELF-HOSTED. It was Material Symbols from Google Fonts - three
   remote links and no font on disk, so every icon in an installed app was one
   lost connection away from disappearing. The file lives beside this stylesheet
   and is versioned with it, so the icons work offline and in the app.

   Phosphor resolves an icon from its NAME as text, the same way Material did,
   so the markup did not change - only the names. The 'liga' feature is what
   turns the word into the glyph; without it the raw name renders as text, which
   is how "savings" once ended up clipped inside a circle. There is no FILL axis
   to set any more: Phosphor ships one file per weight and this is the regular
   one, so the old font-variation-settings line is gone rather than left to
   silently do nothing. */
@font-face {
  /* An @font-face descriptor cannot take var(). This line is what DEFINES the family that the
     --font-icon token then names, so the literal is the source rather than a copy of it - and it
     is read from that token, so the two cannot drift. */
  font-family: 'Phosphor'; /* ds-allow-hardcode */
  src: url('./fonts/Phosphor.woff2') format('woff2');
  font-weight: normal; font-style: normal;
  /* block, not swap: a swap paints the raw name first, so a slow load shows the
     word "arrow-left" where the arrow belongs. An icon that appears late is
     better than a word that appears wrong. */
  font-display: block;
}

/* ---------------------------------------------------------------- the money face
 *
 * THE DISPLAY FIGURE ONLY. Read the note on font.moneyFamily in the token source for why this is
 * one face for one job. The short version: the dots need size, 38px is the measured floor, and
 * below it money stops being readable - so this class exists for the BIG number and the product
 * keeps its text face everywhere else.
 *
 * swap rather than block, like the text faces and unlike the icons: this is text, and the system
 * fallback is a better thing to look at than nothing.
 *
 * The subset carries only the glyphs a money figure is made of, so this face cannot render a
 * sentence even if a screen asks it to - the fallback takes over at the first letter. That is a
 * property of the file rather than a rule someone has to remember. */
@font-face {
  font-family: 'Matrix Sans Print Tabular'; /* ds-allow-hardcode */
  src: url('./fonts/MatrixSansPrintTabular.woff2') format('woff2');
  font-weight: normal; font-style: normal;
  font-display: swap;
}
/* A MONEY FIGURE SET IN THE DISPLAY FACE.
 *
 * FOR THE BIG NUMBER. The floor is 38px (--font-money-min) and this class cannot enforce it: CSS
 * has no minimum-font-size property, and inventing one would be a declaration the browser drops
 * silently while looking like a guarantee. So the floor is stated here, held as a token, and
 * CHECKED - tools/design/audit-money-font.mjs fails any rule that applies this class below it.
 * That is the honest arrangement: the constraint is real, the check is real, and neither pretends
 * the stylesheet can do something it cannot.
 *
 * font-variant-numeric is set even though the shipped file is already tabular, because the
 * FALLBACK is not: without it, the moment this face failed to load, amounts would begin jittering
 * again - which is the exact fault the derived font exists to fix. */
/* A COMPOUND CLASS, DELIBERATELY.
 *
 * .eg-money on its own would promise nothing about size, so the rule that actually applies the
 * face is .eg-money.eg-numxl - the display figure step, which is 38px and up and therefore above
 * the floor. The face and the step it is legal at travel together, and the check above reads the
 * font-size out of this same block rather than trusting whoever writes the next screen.
 *
 * The weight is set to normal on purpose: the face ships one weight, and asking for 600 would make
 * the browser synthesise a bolder one by smearing the dots. */
.eg-money.eg-numxl {
  font-family: var(--font-money);
  font-size: var(--fs-numxl);
  font-weight: normal;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}
.eg-icon {
  font-family: var(--font-icon);
  font-size: var(--c-icon-size);
  font-weight: normal; font-style: normal;
  line-height: 1; letter-spacing: normal; text-transform: none;
  display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr;
  font-feature-settings: 'liga';
  font-variant-ligatures: discretionary-ligatures;
  -webkit-font-smoothing: antialiased;
}
/* PHOSPHOR'S LIGATURE TABLE HAS NO SINGLE-CHARACTER ENTRY, so "x" - the close control on every
   form in the app - draws NOTHING. Measured in the browser: all 54 other names the product uses
   resolve at about 1em; this one measures 0x0, so the icon silently disappears and the layout
   closes over the gap with nothing to show for it.

   The glyph is supplied directly, which is Phosphor's own primary mechanism - its classes inject
   the glyph with :before and the ligature is only the convenience on top. The name stays as the
   element's text, so the icon remains readable, searchable, and checked against the font by
   design:screens. */
/* THE CLOSE GLYPH IS A CODEPOINT, NOT A LIGATURE.
 *
 * Phosphor resolves an icon by ligature, and it has NO single-character ligature - so a
 * button whose icon text is "x" draws nothing at all. No error, no placeholder, no console
 * message: an empty circle where the control should be. The name IS in the font's own list
 * of names, which is why the icon-name gate passes it, so the gate cannot catch this either.
 *
 * The glyph therefore comes from the font's private-use area directly. "cross" renders, but
 * it is literally a Christian cross, and "x-circle" is the already-circled variant, so
 * neither is right for a bare glyph. */
.eg-circle .eg-icon { font-size: var(--c-circle-glyph); }
.eg-icon--x::before { content: "\e4f6"; }
/* A bare close mark, at a size a finger can hit. The visual mark stays small and the target
   is grown by .eg-hit on a coarse pointer, like every other control in the product. */
.eg-x{
  display:inline-flex;align-items:center;justify-content:center;flex:none;
  width:var(--sp-28);height:var(--sp-28);padding:0;border:0;border-radius:var(--r-pill);
  background:rgba(20,24,15,.12);color:inherit;cursor:pointer;
  transition:background var(--t-fast) var(--ease);
}
.eg-x:hover{background:rgba(20,24,15,.22);}
.eg-x.eg-icon{font-size:var(--fs-cap);}
.eg-x--on-dark{background:rgba(255,255,255,.18);}
.eg-x--on-dark:hover{background:rgba(255,255,255,.3);}

/* -------------------------------------------------------------------- tag */
.eg-tag {
  display: inline-flex; align-items: center; background: var(--lime-500); color: var(--ink);
  border-radius: var(--r-sm); padding: var(--c-tag-py) var(--c-tag-px);
  font-size: var(--c-tag-fs); font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}/* A STATUS THAT CANNOT BE CHANGED IS NOT A CONTROL. "Always on" wore the accent, which in this
   system means "press this" - so a fixed state looked like the most pressable thing on the screen.
   It takes the quiet fill instead and reads as what it is: a fact. */
/* A tag's semibold is right for a label that marks a thing; a fixed STATUS is supporting
   information, so it drops to the mute ink and the regular weight. It should not out-shout the
   setting's own name. */
.eg-tag--quiet { background: var(--f-quiet-bg); color: var(--mute); font-weight: var(--fw-regular); }

.eg-dot {
  width: var(--c-dot-size); height: var(--c-dot-size); border-radius: var(--r-pill);
  background: var(--lime-500); box-shadow: 0 0 0 var(--c-dot-ring) var(--lime-100);
}

/* ----------------------------------------------------------------- fields */
.eg-field { display: flex; flex-direction: column; gap: var(--g-inline); }
.eg-field > span, .eg-field > label { font-size: var(--fs-cap); color: var(--mute); }
.eg-input {
  font: inherit; font-size: var(--fs-body); font-weight: var(--fw-medium); color: var(--ink);
  background: var(--raised); border: var(--c-input-border); border-radius: var(--r-md);
  height: var(--c-input-h); padding: var(--sp-0) var(--c-input-px); width: 100%; outline: none;
  transition: border-color var(--t-fast) ease, background var(--t-fast) ease;
}
.eg-input::placeholder { color: var(--mute); font-weight: var(--fw-regular); }
.eg-input:hover { border-color: var(--faint); }
.eg-input:focus { border-color: var(--ink); }
.eg-input[aria-invalid="true"] { border-color: var(--danger); }
.eg-input[disabled] { background: var(--field); color: var(--faint); cursor: not-allowed; }
.eg-error-text { font-size: var(--fs-cap); color: var(--danger); margin: var(--sp-0); }

/* ------------------------------------------------------------------- rows */
.eg-row {
  display: flex; align-items: center; gap: var(--c-row-gap);
  padding: var(--c-row-py) var(--c-row-px); margin: var(--sp-0) var(--c-row-neg-x);
  border-radius: var(--r-md); transition: background var(--t-fast) ease; cursor: pointer;
}
.eg-row:hover { background: var(--hover); }

/* ------------------------------------------------------------------ links */
/* An unstyled <a> renders as the browser default (blue, full underline), which
   is off-brand. Every inline link uses this. */
/* A text link is a target. 19px of inline text is not one, and WCAG 2.5.8 asks for 24 -
   so the line box clears it and the hit area is the row rather than the glyphs. */
.eg-link { min-height: var(--c-touch); display: inline-flex; align-items: center;
  color: var(--ink); font-weight: var(--fw-medium);
  text-decoration: underline; text-underline-offset: var(--sp-2);
  text-decoration-thickness: 1px; text-decoration-color: var(--faint);
  transition: text-decoration-color var(--t-fast) ease;
}
.eg-link:hover { text-decoration-color: var(--ink); }

/* --------------------------------------------------------- step indicator */
.eg-steps { display: flex; align-items: center; gap: var(--c-step-gap); }
.eg-step {
  width: var(--c-step-size); height: var(--c-step-size);
  border-radius: var(--r-pill); background: var(--grey-bar);
}
.eg-step--done { background: var(--lime-500); }
.eg-step--now { background: var(--ink); }

/* ------------------------------------------------------- selectable choice */
/* A SETTINGS ROW: a label, why it matters, and the control that changes it - as a CARD.
   Each setting is its own object, so each gets a card rather than a bare row on the canvas. The
   row it used to be was eg-row, which is a PRESSABLE LIST ITEM: no fill, a negative margin so a
   hover highlight can bleed past the text, and a pointer cursor on the whole row. None of that is
   true of a setting, and borrowing it left four settings floating with no surface and no edges. */
.eg-setrow {
  display: flex; align-items: center; gap: var(--g-sibling);
  background: var(--c-card-bg); border: var(--c-hairline-w) solid var(--border);
  border-radius: var(--c-card-r); padding: var(--c-card-padding);
}
/* The gap between the text and the control is wider than between two pieces of text, because they
   are two different objects meeting rather than two lines of the same one. */
.eg-setrow__copy { flex: 1; min-width: var(--sp-0); display: flex; flex-direction: column; gap: var(--g-inline); }
.eg-setrow__ctl { flex: none; }
.eg-choices { display: flex; flex-wrap: wrap; gap: var(--g-tight); }
/* A CHOICE IS A SELECTED-STATE CONTROL, so it follows the fill rule rather than drawing an edge.
   It carried a 7%-alpha border - the stroke approach that was retired, and the same mistake the
   controls made elsewhere: a hairline that cannot define a control. The fill says the job; the ink
   carries the contrast. */
.eg-choice {
  display: inline-flex; align-items: center; justify-content: center;
  /* A PAIRED CHOICE NEEDS A RESTING FILL. The unselected option was white on a white card, so the
     two read as a selected button beside a stray label rather than as a pair of options. The
     selected one keeps the accent - the system's "this one is chosen" - and the unselected takes
     the inset fill, so it is visible as the thing you would press instead. */
  background: var(--f-inset-bg); color: var(--f-quiet-fg);
  border: none; border-radius: var(--r-pill);
  height: var(--c-choice-h); padding: var(--sp-0) var(--c-choice-px);
  font: inherit; font-size: var(--fs-label); font-weight: var(--fw-medium);
  cursor: pointer; white-space: nowrap;
  transition: background var(--t-fast) ease, color var(--t-fast) ease;
}
/* THE SELECTED STATE IS THE ACCENT. It was the dark fill, which read as "primary action" rather
   than "this one is chosen", and it spent the emphasis budget on a filter. Ink on the accent
   measures 13.73:1, and the selected thing is unmistakable without reading it. */
.eg-choice[aria-pressed="true"] {
  background: var(--f-accent-bg); color: var(--f-accent-fg);
}
.eg-actions-row { display: flex; align-items: center; justify-content: space-between; gap: var(--g-tight); }

/* ---------------------------------------------------------------- avatar */
.eg-avatar {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: var(--c-avatar-size); height: var(--c-avatar-size);
  border-radius: var(--r-pill); background: var(--field); color: var(--ink);
  font-size: var(--fs-cap); font-weight: var(--fw-semibold); line-height: 1;
}
.eg-avatar--mine, .eg-avatar--guru { background: var(--dark); color: var(--ink-inv); }
.eg-avatar--lg { width: var(--c-avatar-size-lg); height: var(--c-avatar-size-lg); font-size: var(--c-avatar-fs-lg); }

/* --------------------------------------------------------------- tab bar */
/* A floating pill, fixed to the bottom by whatever scrolls behind it.
   NO SHADOW: the product is flat by rule, so the lift comes from the inset and the
   radius and the edge is a border, which is structure rather than depth. */
.eg-tabbar {
  display: flex; align-items: center; gap: var(--c-tabbar-gap);
  background: var(--c-tabbar-bg); border: var(--c-tabbar-border);
  border-radius: var(--c-tabbar-r); padding: var(--c-tabbar-pad);
  margin: var(--sp-0) var(--c-tabbar-inset);
}
/* Fixed to the bottom of the viewport, for a client where the bar is not a flex child.
   The prototype stacks it, the app pins it; same component, same tokens. */
.eg-tabbar--fixed { position: fixed; left: var(--sp-0); right: var(--sp-0); bottom: var(--sp-12); z-index: 20; }

.eg-tab {
  flex: 1; position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-0);
  padding: var(--c-tabbar-py) var(--sp-0);
  border-radius: var(--c-tabbar-r); min-width: 0;
  color: var(--c-tabbar-fg); background: none; border: none; font: inherit; cursor: pointer;
  font-size: var(--c-tabbar-label);
  transition: background var(--t-fast) ease, color var(--t-fast) ease, gap var(--t-fast) ease;
}
/* Four or five destinations do not fit a 280px screen at a 44px floor each. Below 400
   the floor drops to the WCAG 24px minimum, which is the smallest target that is
   still legal, rather than letting the bar push the page sideways. */
/* Below 400 the floor is the WCAG 24px minimum rather than 44. It is a floor on the TARGET,
   so it is applied through a pseudo-element that does not affect layout. As a min-width on the
   tab itself it made the bar wider than the display on a 280px screen. */
@media (max-width: 400px) { .eg-tab::after { content: ''; position: absolute; inset: 50% 0 auto 0;
  height: var(--c-touch-min); transform: translateY(-50%); } }
.eg-tab .eg-icon { font-size: var(--c-tabbar-icon); }
/* HOVER LIFTS TOWARD THE FOREGROUND, WHICHEVER WAY THAT IS.
 *
 * This was a plain colour: var(--ink). On v1's white bar that was correct: ink is the strongest text
 * colour and hover made an inactive tab as readable as the selected one. v2 made the bar DARK, so
 * the same declaration painted ink on ink - both rgb(20,24,15), a measured 1.00:1, so hovering a
 * destination made its icon VANISH. The state-aware gate caught it on eight screens.
 *
 * The selected tab is the one case that must not change, because its own colour is already the
 * accent on the accent pill; --c-tabbar-on-fg is a token for exactly that and hover on it is
 * excluded by the rule below. */
.eg-tab:hover { color: var(--c-tabbar-hover-fg); }
.eg-tab--on:hover, .eg-tab[aria-current="page"]:hover { color: var(--c-tabbar-on-fg); }
/* The label exists for every destination and is only SHOWN for the selected one.
   Clipped, not removed: an icon-only tab still needs a name a screen reader can read. */
.eg-tab__label {
  max-width: var(--sp-0); overflow: hidden; white-space: nowrap; opacity: 0;
  font-size: var(--c-tabbar-label); font-weight: var(--fw-semibold);
  transition: max-width var(--t-fast) ease, opacity var(--t-fast) ease;
}
.eg-tab--on, .eg-tab[aria-current="page"] {
  background: var(--c-tabbar-on-bg); color: var(--c-tabbar-on-fg); gap: var(--c-tabbar-on-gap);
  padding-inline: var(--c-tabbar-px);
  /* The selected tab takes the width its NAME needs; the rest share the remainder.
     Equal widths clip "Notifications" at five destinations, and the reference design
     shows the selected pill wider for this reason. */
  flex: 0 1 auto;
}
.eg-tab--on .eg-tab__label, .eg-tab[aria-current="page"] .eg-tab__label {
  max-width: var(--c-tabbar-label-max); opacity: 1;
}
@media (prefers-reduced-motion: reduce) { .eg-tab, .eg-tab__label { transition: none; } }
/* The icon and its badge move together, so the badge stays on the icon corner
   whether or not the label is showing. */
.eg-tab__ico { position: relative; display: inline-flex; flex: none; }
.eg-tab__badge {
  position: absolute; top: var(--c-tabbar-badge-y); left: calc(50% + var(--c-tabbar-badge-x));
  min-width: var(--c-badge-minw); height: var(--c-badge-h); padding: 0 var(--c-badge-px);
  border-radius: var(--r-pill); background: var(--c-badge-bg); color: var(--c-badge-fg);
  font-size: var(--c-badge-fs); font-weight: var(--c-badge-fw);
  display: inline-grid; place-items: center; font-variant-numeric: tabular-nums;
}

/* -------------------------------------------------- balance / money rows */
.eg-bals { display: flex; flex-direction: column; }
.eg-bal {
  display: flex; align-items: center; justify-content: space-between; gap: var(--g-tight);
  padding: var(--sp-12) var(--sp-0);
}
.eg-bal + .eg-bal { border-top: 1px solid var(--hairline); }
.eg-bal__who { display: flex; align-items: center; gap: var(--g-tight); min-width: var(--sp-0); }
.eg-bal__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ------------------------------------------- a claim that blocks on a user */
/* A CLAIM IS A CARD, so it wears the CARD step - radius AND padding together, from the same pair.
   It was radius 18px (a well's step) with 16px padding, against the card's 28px and 28px. A
   component that wears a card's shape with a well's proportions reads as a different kind of object
   on the screen, which is exactly what the owner saw next to the home screen's cards. */
.eg-claim {
  display: flex; flex-direction: column; gap: var(--g-tight);
  padding: var(--c-card-padding);
  /* The card's own surface and its hairline. Both were changed while making it wear the card step,
     and dropping the border removed the card's edge entirely: a near-white surface on a near-white
     canvas has no boundary without it. */
  background: var(--c-card-bg); border: var(--c-hairline-w) solid var(--border); border-radius: var(--c-card-r);
}
.eg-claim__acts { display: flex; gap: var(--g-tight);  flex-wrap: wrap; }
.eg-claim__acts > * { flex: 1; }

/* ------------------------------------------------------- a Room feed entry */
/* A row can be a button, so it carries the reset: no user-agent border, fill or centring.
   Left-aligned because a list of records reads from the start edge, never from the middle. */
.eg-event { display: flex; align-items: center; gap: var(--g-tight); padding: var(--sp-12) var(--sp-0);
  width: 100%; font: inherit; text-align: start; color: var(--ink);
  background: none; border: 0; border-top: var(--c-hairline-w) solid var(--hairline); cursor: pointer; }
.eg-event:first-child { border-top: 0; }
.eg-event:hover { background: var(--hover); }
.eg-event__main { flex: 1; min-width: var(--sp-0); display: flex; flex-direction: column; gap: var(--g-inline); }
/* A GROUP OF ROWS AS ONE CARD.
   A settings list is not a ledger. As bare rows on the canvas they read as a repeated line-list -
   a divider, a gap, a divider - which is the look of a table, not of grouped choices. Wrapping the
   rows in the card shell gives the group one surface and one edge, the way the rest of the product
   groups things; the hairline keeps doing its job INSIDE the card, separating row from row. */
.eg-card--list { padding: var(--c-card-padding); }
.eg-card--list > .eg-event {
  padding: var(--sp-12) var(--sp-0);
  border-top: var(--c-hairline-w) solid var(--border);
}
/* The card keeps its own padding on all four sides. The first and last rows give up the padding
   that would DOUBLE against it - inside the card the row's vertical padding is spacing between
   rows, and there is no row above the first or below the last for it to separate. */
.eg-card--list > .eg-event:first-child { border-top: 0; padding-top: var(--sp-0); }
.eg-card--list > .eg-event:last-child { padding-bottom: var(--sp-0); }

/* ------------------------------------------------------- a thread message */
/* Money is darker. That is the whole distinction: an expense must never be
   mistakable for someone saying hello. */
.eg-bubble {
  display: flex; flex-direction: column; gap: var(--g-inline);
  max-width: var(--c-bubble-max); padding: var(--c-bubble-py) var(--c-bubble-px);
  border-radius: var(--c-bubble-r); background: var(--c-bubble-bg); color: var(--c-bubble-fg);
}
.eg-bubble--mine { align-self: flex-end; }
.eg-bubble--money { background: var(--c-bubble-money-bg); color: var(--c-bubble-money-fg); }
.eg-bubble__meta { font-size: var(--fs-tiny); opacity: .8; }
.eg-bubble__body { font-size: var(--fs-body); line-height: 1.45; overflow-wrap: anywhere; }
.eg-bubble--money .eg-bubble__amount { font-size: var(--fs-title); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
/* Who is in on it. "Split 3 ways" is a count; a count is not transparency, and
   the first question about a disputed expense is always "who was in on this". */
.eg-bubble__who { font-size: var(--fs-tiny); opacity: .85; }
.eg-bubble--tappable { text-align: left; border: 1px solid transparent; font: inherit; cursor: pointer; }
.eg-bubble--tappable:hover { border-color: var(--ink-inv); }

/* A message that became an expense: the words are kept, and they are the record. */
.eg-quote {
  display: flex; flex-direction: column; gap: var(--g-inline); text-align: left; width: 100%;
  background: var(--field); border: none; border-left: var(--c-tag-py) solid var(--faint);
  border-radius: var(--c-card-r); padding: var(--c-card-padding);
  font: inherit; color: var(--mute); cursor: pointer;
}
.eg-quote:hover { border-left-color: var(--ink); }
.eg-quote__body { font-size: var(--fs-cap); line-height: 1.45; overflow-wrap: anywhere; }

/* -------------------------------------------------------------- a split card */
/* The one reading, used twice: in the composer before anything is sent, and
   opened from the expense afterwards. Same numbers, same engine, both places. */
.eg-split {
  display: flex; flex-direction: column; gap: var(--c-split-gap); text-align: left;
  background: var(--c-split-bg); border: 1px solid var(--c-split-border);
  border-radius: var(--c-split-r); padding: var(--c-split-py) var(--c-split-px);
}
.eg-split__head { font-size: var(--fs-cap); color: var(--mute); }
.eg-split__amount { font-size: var(--c-split-amount); font-weight: var(--fw-semibold);
                    font-variant-numeric: tabular-nums; line-height: 1; }
.eg-split__rows { display: flex; flex-direction: column; gap: var(--c-split-row-gap); }
.eg-split__row { display: flex; align-items: baseline; gap: var(--g-tight); font-size: var(--fs-cap); }
.eg-split__row > b { font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.eg-split__row > span:last-child { margin-left: auto; color: var(--mute); }

/* ------------------------------------------------------------- the composer */
.eg-composer {
  display: flex; align-items: center; gap: var(--c-composer-gap);
  background: var(--c-composer-bg); border: 1px solid var(--c-composer-border);
  border-radius: var(--c-composer-r); padding: var(--c-composer-py) var(--c-composer-px);
  min-height: var(--c-composer-h);
}
.eg-composer .eg-input { border: none; background: none; padding: var(--sp-0); height: auto; min-height: var(--sp-24); }
/* The ring moves to the composer. The field keeps its own focus removed rather
   than losing focus visibility: this one change is what stops the double box,
   and the composer still shows focus to a keyboard user, same width and offset
   as every other control in the product. */
.eg-composer .eg-input:focus-visible { outline: none; }
/* No highlight ring on the composer. For a text field the caret is the focus
   indicator, so the ring was decoration drawn on top of one. The border darkening
   keeps focus visible for the eye without boxing the control in. */
.eg-composer:focus-within { outline: none; border-color: var(--ink); }

/* ---------------------------------------------------- the device frame -- */
/* ---------------------------------------------------------- components ---- */
/* SWIPE ACTION and LABELLED FIELD live here, not in the app.

   They were in src/App.jsx, which meant the design system documented two components it did not own
   and could not render - and the page's own gate refused to emit until they appeared on it, which
   is how this was found. A component's styles belong with the components. */

/* Swipe action. The panel is a CHILD of the row and parked just past its right edge, so it travels
   with the row and the wrapper clips it until the row slides left. It must not be a positioned
   sibling: absolutely positioned elements paint above static ones whatever the DOM order, which put
   it over the amount. Destructive, so it wears --danger. */
.eg-swipe { position: relative; overflow: hidden; margin: var(--sp-0) calc(var(--sp-8) * -1); border-radius: var(--r-md); }
.eg-swipe > .eg-row { margin: var(--sp-0); background: var(--surface); touch-action: pan-y; position: relative;
  transition: transform var(--t-fast) var(--spring), background var(--t-fast) ease; }
.eg-swipe > .eg-row:hover { background: var(--hover); }
.eg-swipe__del { position: absolute; top: var(--sp-0); left: 100%; bottom: var(--sp-0); width: var(--c-swipe-w);
  display: flex; align-items: center; justify-content: center;
  background: var(--grey-cell); color: var(--danger); border-radius: var(--r-md); pointer-events: none; }

/* Labelled field. The label minimum is what makes a pair wrap rather than squeeze, and each label
   is its own .eg-field so the caption stays associated with its control, for a screen reader and
   for a tap on the words. */
.eg-two { display: flex; flex-wrap: wrap; gap: var(--g-control); }
.eg-two > label { flex: 1; min-width: var(--c-label-min); }

/* Prototype chrome, not a product component. It exists so the app is judged at
   a real viewport: what is above the fold, what scrolls, and what stays put. */
.eg-frame {
  position: relative; flex: none;
  width: calc(var(--c-device-w) + var(--c-device-bezel) * 2);
  height: calc(var(--c-device-h) + var(--c-device-bezel) * 2);
  padding: var(--c-device-bezel); border-radius: var(--c-device-r);
  background: var(--ink);
}
.eg-frame__screen {
  position: relative; width: var(--c-device-w); height: var(--c-device-h);
  border-radius: var(--c-device-screen-r); overflow: hidden; background: var(--surface);
  display: flex; flex-direction: column;
}
/* the island sits over the status bar, as it does on the device */
.eg-frame__island {
  position: absolute; top: calc(var(--c-device-bezel) + var(--c-device-island-top)); left: 50%; transform: translateX(-50%);
  width: var(--c-device-island-w); height: var(--c-device-island-h);
  border-radius: var(--r-pill); background: var(--ink); z-index: 4;
}
.eg-statusbar {
  position: absolute; top: 0; left: 0; right: 0; height: var(--c-device-statusbar); z-index: 3;
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-0) var(--sp-28); pointer-events: none;
  font-size: var(--fs-cap); font-weight: var(--fw-semibold); color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.eg-statusbar__right { display: inline-flex; align-items: center; gap: var(--g-inline); }
.eg-statusbar .eg-icon { font-size: var(--fs-title); }
/* the home indicator, drawn over the content the way the system does */
.eg-frame__homebar {
  position: absolute; bottom: var(--c-device-homebar-bottom); left: 50%; transform: translateX(-50%); z-index: 4;
  width: var(--c-device-homebar-w); height: var(--c-device-homebar-h);
  border-radius: var(--r-pill); background: var(--ink); opacity: .28;
}

/* ------------------------------------------------- the Room's surfaces */
/* One inset for the whole Room chrome: the header, this row and the body all start at
   the same edge. Before, this row ran edge to edge while both neighbours were inset
   20px, which is the stray edge better-layout warns about. */
.eg-roomtabs {
  margin: var(--sp-0) var(--c-roomtabs-inset);
  display: flex; gap: var(--c-roomtabs-gap); background: var(--c-roomtabs-bg);
  border-radius: var(--c-roomtabs-r); padding: var(--c-roomtabs-p);
}
.eg-roomtab {
  /* The same rule as .eg-tab, for the same reason: a flex item defaults to min-width:auto and so
     refuses to shrink below its own label. Three Room tabs whose names are longer than their share
     of a narrow screen pushed the row past the display. */
  flex: 1; min-width: 0; display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--c-touch);
  font: inherit; font-size: var(--fs-cap); border: none; background: none;
  color: var(--c-roomtabs-fg); cursor: pointer; border-radius: var(--c-roomtabs-r);
  padding: var(--c-roomtabs-tab-py) var(--c-roomtabs-tab-px);
}
.eg-roomtab[aria-selected="true"] { background: var(--c-roomtabs-on-bg); color: var(--c-roomtabs-on-fg); }

/* ------------------------------------------------------- the mention popup */
/* ---------------------------------------------------------------- switch */
/* TWO MODES, ONE CONTROL, ONE STATE. The chosen side is filled; the other is quiet. The whole
   thing is one pill so it reads as a switch rather than as two buttons that happen to be
   adjacent, which is what tells a person that choosing one unchooses the other. */
.eg-switch {
  display: inline-flex; align-items: center; gap: var(--c-switch-gap);
  height: var(--c-switch-h); padding: var(--c-switch-pad);
  border-radius: var(--c-switch-p-r); background: var(--c-switch-bg);
}
.eg-switch__opt {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  height: 100%; padding: 0 var(--c-switch-px);
  border: 0; border-radius: var(--c-switch-p-r); background: none;
  color: var(--c-switch-fg); font: inherit; font-size: var(--c-switch-fs);
  font-weight: var(--fw-medium); cursor: pointer; white-space: nowrap;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.eg-switch__opt:hover { color: var(--ink-inv); }
.eg-switch__opt[aria-pressed="true"] { background: var(--c-switch-on-bg); color: var(--c-switch-on-fg); font-weight: var(--fw-semibold); }
.eg-switch__opt:focus-visible { outline: var(--c-focus-w) solid var(--lime-500); outline-offset: var(--c-focus-offset); }

/* A small panel floating over its trigger. See the popover token for why this is the one
   thing in the product allowed a shadow. */
.eg-pop {
  position: absolute; z-index: 30;
  width: var(--c-pop-w); max-width: var(--c-pop-maxw);
  display: flex; flex-direction: column; gap: var(--c-pop-gap);
  background: var(--c-pop-bg); color: var(--ink);
  border: var(--c-pop-border); border-radius: var(--c-pop-r);
  padding: var(--c-pop-pad);
  box-shadow: var(--c-pop-shadow);
}
/* The little arrow, drawn with a rotated square so it inherits the same border and
   background without a second image or a second colour to keep in step. */
/* A row is a label and a value on ONE line, pushed apart. Without this the label wraps to
   three lines while the value it belongs to sits beside it, reading as two unrelated
   columns. The row is part of the popover, not something each caller restates. */
.eg-pop__row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--c-pop-gap); font-size: var(--fs-cap);
}
.eg-pop__row > span:first-child { white-space: nowrap; color: var(--mute); }
.eg-pop__row > b { white-space: nowrap; font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.eg-pop--below::before {
  content: ''; position: absolute; top: calc(var(--sp-6) * -1); left: var(--sp-24);
  width: var(--sp-12); height: var(--sp-12); transform: rotate(45deg);
  background: var(--c-pop-bg); border-left: var(--c-pop-border); border-top: var(--c-pop-border);
  border-top-left-radius: var(--r-xs);
}

.eg-suggest {
  display: flex; flex-direction: column; background: var(--c-suggest-bg);
  border: 1px solid var(--c-suggest-border); border-radius: var(--c-suggest-r); overflow: hidden;
}
/* A list the app opens and closes with the hidden attribute. display:flex above outranks the
   attribute's own display:none, so without this a closed list is simply an empty box. */
.eg-suggest[hidden] { display: none; }
.eg-sug {
  display: flex; align-items: center; gap: var(--c-suggest-gap); width: 100%; text-align: left;
  background: none; border: none; font: inherit; color: var(--ink); cursor: pointer;
  padding: var(--c-suggest-row-py) var(--c-suggest-row-px);
}
.eg-sug + .eg-sug { border-top: 1px solid var(--hairline); }
.eg-sug[aria-selected="true"] { background: var(--c-suggest-on); }
.eg-sug__name { flex: 1; min-width: 0; }

/* ------------------------------------------------------------------ @mention */
.eg-mention {
  display: inline-block; border-radius: var(--c-mention-r);
  padding: var(--c-mention-py) var(--c-mention-px);
  background: var(--c-mention-bg); color: var(--c-mention-fg);
  font-weight: var(--c-mention-weight); white-space: nowrap;
}

/* --------------------------------------------------------- a zero-data state */
.eg-empty {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--g-tight); padding: var(--c-empty-py) var(--sp-16);
}

/* ------------------------------------------------------------- skeleton */
/* Preferred over a spinner where the layout is predictable: the page does not
   jump when real data arrives. Show only after the token delay. */
.eg-skel { display: flex; flex-direction: column; gap: var(--c-skel-gap); }
.eg-skel__line { height: var(--c-skel-h); border-radius: var(--r-sm); background: var(--grey-cell); }
.eg-skel__line--wide { width: 100%; }
.eg-skel__line--mid { width: 72%; }
.eg-skel__line--tall { height: var(--c-circle-size); border-radius: var(--r-md); }

/* --------------------------------------------------------------- two-up row */
.eg-pair { display: flex; flex-wrap: wrap; gap: var(--g-tight); }
.eg-pair > * { flex: 1 1 auto; min-width: 0; }
.eg-pair > * { flex: 1; }

/* --------------------------------------------------------------- select */
/* Same shell as .eg-input; only the native affordance differs. */
.eg-select {
  font: inherit; font-size: var(--fs-body); font-weight: var(--fw-medium);
  color: var(--ink); background: var(--raised);
  border: var(--c-input-border); border-radius: var(--r-md);
  height: var(--c-input-h); padding: var(--sp-0) var(--c-input-px);
  width: 100%; outline: none; cursor: pointer;
  appearance: none;
}
.eg-select:focus-visible { border-color: var(--ink); }

/* ---------------------------------------------------------------- toast */
.eg-toast {
  position: relative; overflow: hidden;
  /* Hug the content, never stretch. A column flex parent would otherwise stretch
     the toast to full width and the ring would run end to end around empty
     space. max-width still caps it, so long text wraps rather than overflows. */
  display: inline-flex; width: fit-content; align-items: center; gap: var(--g-tight);
  background: var(--dark); color: var(--ink-inv);
  border-radius: var(--r-pill); padding: var(--c-toast-pad);
  font-size: var(--fs-cap); max-width: var(--c-toast-max);
}

/* The undo window, drawn as a RING that sweeps clockwise from twelve o'clock.
   The pill's own shape carries the stroke, so there is no geometry to measure
   and it works at any toast width.

   Built without SVG and without mask compositing:
     - .eg-toast__ring is a full conic gradient, clipped to the pill
     - a mask punches out the content box, leaving a stroke of exactly
       --c-toast-ring-w at the edge
     - the transparent arc GROWS clockwise, eating the stroke like a clock

   --eg-toast-gap is animated through @property so the angle interpolates.
   Without @property support the ring simply does not draw; the toast itself is
   unaffected, so the degradation is graceful. */
@property --eg-toast-gap { syntax: '<angle>'; initial-value: 0deg; inherits: false; }

.eg-toast .eg-toast__ring {
  position: absolute; inset: var(--sp-0); border-radius: inherit;
  pointer-events: none;
  background: conic-gradient(from -90deg,
    transparent 0 var(--eg-toast-gap),
    var(--lime-500) var(--eg-toast-gap));
  animation: eg-toast-sweep var(--t-toast) linear forwards;
}
/* A true stroke, not a fill with a lid: masking out the content box leaves
   exactly the ring, so nothing overlaps and there is no seam at the curve. */
.eg-toast .eg-toast__ring {
  padding: var(--c-toast-ring-w);
  /* #000 here is an ALPHA MARKER, not a colour: only its opacity matters and the
     mask is composited away. Any opaque value behaves identically. */ /* ds-allow-hardcode */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); /* ds-allow-hardcode */
  mask-composite: exclude;
}
@keyframes eg-toast-sweep {
  from { --eg-toast-gap: 0deg; }
  to   { --eg-toast-gap: 360deg; }
}
/* The message and the button sit above the ring. The ring is excluded by
   NAME, not by source order, so this can never re-position it again. */
.eg-toast > :not(.eg-toast__ring) { position: relative; z-index: 1; }

/* Hover or focus buys more time: the accessible answer to WCAG 2.2.1, where a
   time limit must be extendable. The JS dismiss timer must pause too, or the
   ring and the behaviour would disagree. */
.eg-toast:hover .eg-toast__ring,
.eg-toast:focus-within .eg-toast__ring { animation-play-state: paused; }
.eg-toast__action { background: var(--raised); color: var(--ink); }

/* ------------------------------------------------------------------ bell -- */
/* The count is action-needed ONLY, and it does not render at zero. A badge that can
   show nothing teaches people to stop reading it; one that counts everything is the
   same failure with more pixels. */
.eg-bell { position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: var(--c-bell-size); height: var(--c-bell-size); flex: none;
  background: var(--c-bell-bg); border: var(--c-bell-border); border-radius: var(--r-pill);
  padding: var(--sp-0); cursor: pointer; color: var(--ink); }
.eg-bell .eg-icon { font-size: var(--c-bell-icon); }
.eg-bell__n { position: absolute; top: var(--sp-2); right: var(--sp-2);
  min-width: var(--c-bell-badge-minw); height: var(--c-bell-badge-h); padding: var(--sp-0) var(--c-bell-badge-px);
  border-radius: var(--r-pill); background: var(--c-bell-badge-bg); color: var(--c-bell-badge-fg);
  font-size: var(--fs-tiny); font-weight: var(--c-bell-badge-fw);
  display: inline-grid; place-items: center; font-variant-numeric: tabular-nums;
  box-shadow: 0 0 0 var(--c-bell-badge-ring) var(--c-bell-badge-ringc); }

/* -------------------------------------------------------------- fuel bar
 *
 * THE TRACK IS DARK, AND THAT IS A CONTRAST DECISION RATHER THAN A TASTE ONE.
 *
 * The fill is the accent, and the accent is a surface: the vivid lime measures 1.00:1 against the
 * old grey track, which means the filled fraction of the bar was literally invisible against the
 * empty one - the one thing a progress bar exists to show. It cannot be fixed by darkening the
 * lime either, because the lime then stops being lime.
 *
 * So the track inverts instead: dark ground, bright fill. Lime on deep measures 10.7:1, the two
 * ends of the bar are unmistakable, and the bar finally reads the way the rest of v2 does - the
 * accent as the big shape, a dark ground carrying it. The pace tick is ink, which reads on the
 * dark track at 14:1. */
.eg-fuel { position: relative; height: var(--c-fuel-h); border-radius: var(--r-pill);
           background: var(--c-fuel-track); }
.eg-fuel__fill { position: absolute; inset: var(--sp-0) auto var(--sp-0) var(--sp-0);
                 border-radius: var(--r-pill); background: var(--lime-500); }
/* Low fuel is the SAME fill with the warning hatch across it. The hatch used to be written as a
   var() naming a token the stylesheet never defined, so the whole background declaration was
   invalid and the bar silently fell back to plain lime - the low-fuel signal did nothing. The
   gradient is spliced in from the token source instead, which is where it actually lives. */
/* The hatch's own geometry is the one raw length in this palette. A gradient stop is a
   measurement rather than a relationship - a 2px stripe every 7px describes the pattern, the way
   a control's internal padding describes the control - so it cannot be a gap token. It is
   declared in design-tokens.js and emitted here with the exemption marked ON THE LINE, because
   that is where the gate reads it. */
.eg-fuel__fill--low { background: repeating-linear-gradient(135deg, rgba(20,24,15,.12) 0px, rgba(20,24,15,.12) 2px, transparent 2px, transparent 7px), var(--lime-500); } /* ds-allow-hardcode: hatch geometry, declared in design-tokens.js */
.eg-fuel__tick { position: absolute; top: calc(var(--sp-4) * -1); bottom: calc(var(--sp-4) * -1);
                 width: var(--c-fuel-tick); background: var(--ink); }

/* -------------------------------------------------- tag, hero size */
/* The complement of .eg-tag: outlined, with a leading icon. Two existing tokens, no new
   colour, and no alpha - a translucent lime over a dark bubble and over canvas would be two
   different colours, and a token is the same colour everywhere. */
.eg-tag--outline { background: var(--c-tag-ol-fill); border: var(--c-hairline-w) solid var(--c-tag-ol-stroke);
  color: var(--ink); gap: var(--c-tag-ol-gap); font-size: var(--c-tag-ol-fs); font-weight: var(--fw-medium); }
.eg-tag--outline .eg-icon { font-size: var(--c-tag-ol-icon); }

.eg-tag--hero { font-size: var(--c-tag-hero-fs); padding: var(--c-tag-hero-py) var(--c-tag-hero-px); }

/* ------------------------------------------------------ inline dot + text */
.eg-dot-wrap { display: inline-flex; align-items: center; gap: var(--g-tight); }

/* Disabled: a surface change, never an opacity fade. Fading the whole control
   drops its label to about 2.9:1, which is unreadable. Field plus mute keeps
   the text legible (mute on field measures 4.9:1) while the control still
   reads as inactive. */
.eg-pill:disabled, .eg-select:disabled, .eg-input:disabled {
  background: var(--field); color: var(--mute); cursor: not-allowed; opacity: 1;
}
.eg-pill:disabled:hover { transform: none; background: var(--field); }
.eg-pill:disabled:focus-visible { outline: none; }

/* ------------------------------------------------------------------ focus */
/* Every interactive control, in one place. .eg-choice and .eg-tab were missing
   from this list, so they fell back to the browser default ring - a different
   colour, width and offset from the rest of the product. */
.eg-pill:focus-visible,
.eg-circle:focus-visible,
/* A FIELD IS NOT RINGED. The input's own focus rule already darkens the border, and the ring was
   a SECOND indicator on the same control - which on a rounded rectangle reads as two
   nested boxes rather than as focus. It was always there; v2 made it obvious, because the
   border went from 1px to 1.5px and the radius from 16 to 18, so the inner rectangle
   stopped hiding inside the outer one. The border is the focus treatment for a field; the
   ring is for a control with no border of its own. */
.eg-select:focus-visible,
.eg-choice:focus-visible,
.eg-tab:focus-visible,
.eg-row:focus-visible,
a:focus-visible {
  outline: var(--c-focus-w) solid var(--ink);
  outline-offset: var(--c-focus-offset);
}

/* -------------------------------------------------------------- utilities */
.eg-stack { display: flex; flex-direction: column; }
.eg-center { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: var(--sp-16); }
.eg-visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: var(--sp-0); margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ------------------------------------------------------- screen scaffolds */
/* A single-column, centred panel: auth, settings, empty or error screens. */
.eg-panel { width: 100%; max-width: var(--c-narrow); }
.eg-panel__body { padding: var(--c-card-padding); display: flex; flex-direction: column; gap: var(--g-sibling); }
.eg-panel__head { display: flex; flex-direction: column; gap: var(--g-tight); }
.eg-panel__form { display: flex; flex-direction: column; gap: var(--g-control); }
.eg-brand { display: flex; align-items: center; gap: var(--g-tight); }
.eg-actions { display: flex; flex-direction: column; gap: var(--g-tight); }
/* A tag inside a flex column stretches; wrap it to hug its content. */
.eg-tag-line { margin: var(--sp-0); align-self: flex-start; }

/* Async indicator. Icon-sized, token-driven, stopped by reduced-motion. */
.eg-spinner {
  width: var(--c-icon-size); height: var(--c-icon-size); flex: none;
  border: var(--c-focus-w) solid currentColor; border-top-color: transparent;
  border-radius: var(--r-pill); animation: eg-spin var(--t-slow) linear infinite;
}
@keyframes eg-spin { to { transform: rotate(360deg); } }

/* Diagnostic readout, e.g. the names of missing environment variables. */
.eg-code-block {
  font-family: var(--font-mono);
  font-size: var(--fs-cap); background: var(--field);
  border-radius: var(--r-sm); padding: var(--sp-10) var(--sp-12);
}

/* --------------------------------------------------------- accessibility */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
}
@media (pointer: coarse) {
  .eg-pill, .eg-circle { position: relative; }
  .eg-pill::after, .eg-circle::after {
    content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: 100%; height: 100%; min-width: var(--c-touch); min-height: var(--c-touch);
  }
}
