/* Rotunda's one stylesheet.
 *
 * Extracted (2026-09) from app.py, where it lived as an ~865-line
 * Python string constant named STYLE. Comments elsewhere in app.py
 * that say "see STYLE" mean this file; app.py still reads it into a
 * constant of that name at import, and serves it at /static/style.css.
 *
 * PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBkPSJNMjAgNDIgQTMwIDMwIDAgMCAxIDgwIDQyIiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMTAiLz4KICA8cmVjdCB4PSI4IiB5PSI0NyIgd2lkdGg9Ijg0IiBoZWlnaHQ9IjciLz4KICA8cmVjdCB4PSIxOCIgeT0iNTkiIHdpZHRoPSI5IiBoZWlnaHQ9IjI3Ii8+CiAgPHJlY3QgeD0iMzUiIHk9IjU5IiB3aWR0aD0iOSIgaGVpZ2h0PSIyNyIvPgogIDxyZWN0IHg9IjUyIiB5PSI1OSIgd2lkdGg9IjkiIGhlaWdodD0iMjciLz4KICA8cmVjdCB4PSI2OSIgeT0iNTkiIHdpZHRoPSI5IiBoZWlnaHQ9IjI3Ii8+CiAgPHJlY3QgeD0iMTQiIHk9IjkxIiB3aWR0aD0iNzIiIGhlaWdodD0iNyIvPgo8L3N2Zz4= below is not a CSS feature and not a typo:
 * app.py substitutes the base64-encoded brand mark into it at import
 * time (see BRAND_MARK_SVG there), so opening this file directly shows
 * the placeholder rather than the icon.
 */
  :root {
    /* Rotunda visual redesign (2026-08) — replaces the monochrome
       palette that itself replaced an earlier indigo/Linear one (see
       git history: #27 tried reintroducing color on secondary buttons,
       #30 reverted it). This is a deliberate, larger reversal of that
       revert, built from a Claude Design mockup the team approved —
       not a regression back to #27's version. Same token *names* as
       before on purpose, so the hundreds of existing rules below that
       reference --ink/--paper/--slate/etc. by name pick up the new
       values for free; only the values themselves, plus a handful of
       new tokens (--bg, --gold, --warn, --info, --radius-lg), change
       here. --content-bg is the sidebar-shell (app_shell()) page
       background — in this design it's intentionally the SAME value as
       --paper (the chrome bg), since the mockup has no separate "bar"
       color at all: sidebar/topbar/content all sit on one flat --bg,
       divided only by 1px hairlines (--rule). --surface is the one
       token that still reads as a distinct "raised" tone (cards/panels/
       dropdowns/modals) — the mockup's own cards are actually
       transparent+border, but keeping --surface non-transparent for
       now avoids relitigating every .card/.panel rule in this pass;
       see the redesign follow-up for per-page component work. */
    /* v5 (Rotunda Dashboard v5 mockup): a warm grey canvas (--bg) under a
       lighter sheet (--paper, the chrome), a sidebar a step darker than
       the sheet, white cards edged in --rule-card. */
    --bg: #e6e4dd; --ink: #14181f; --paper: #fbfaf7; --content-bg: #fbfaf7; --surface: #ffffff;
    --sidebar: #f3f2ee; --ink-2: #5b6270; --muted: #5c6370; --link: #1f3a63;
    --gold: #8A6A18;
    --slate: color-mix(in srgb, var(--ink) 65%, transparent);
    --rule: color-mix(in srgb, var(--ink) 9%, transparent); /* ≈ #e7e5de on the sheet */
    /* --rule at 14% measures 1.35:1 against either ground. Fine for a
       divider — WCAG 1.4.11 doesn't govern decoration — and not fine for
       the visual boundary of a control, which is what tells a
       low-vision reader an input is there at all and needs 3:1. So the
       hairline that separates rows and the outline that IS the control
       are now two tokens.

       48% is where light mode clears the bar: #111 at that alpha on the
       cream ground measures 3.29:1, and the same alpha of white on black
       measures 4.92:1. Light is the binding case by a wide margin, which
       is why the number looks heavier than a dark-mode-only reading
       would suggest. Measured, not guessed, and re-measured on every
       test run — see tests/test_contrast.py. */
    --rule-strong: color-mix(in srgb, var(--ink) 48%, transparent);
    /* And a third weight, between the two, for the border that is the
       whole outline of a card rather than a divider inside one. A card
       is not a control, so 1.4.11's 3:1 doesn't govern it and
       --rule-strong reads far too heavy at card size — but --rule's
       1.35:1 is close to invisible in bright outdoor light, which is
       the reading condition for someone checking a hearing between
       committee rooms. 30% measures ~2:1 on the cream ground and
       ~2.5:1 on black: clearly above the hairline it replaces, clearly
       below the outline that means "this takes input". Measured on
       every test run — see tests/test_contrast.py.

       The exception is the mobile bill card, which uses --rule-strong:
       there the border isn't decoration around content that has other
       boundaries, it is the ONLY thing separating one bill from the
       next. */
    --rule-card: color-mix(in srgb, var(--ink) 30%, transparent);
    --accent: var(--link);
    /* --accent-solid/--accent-solid-text are the button-fill pair — in
       dark mode the fill inverts (light pill, dark text) while --accent
       above stays a light-on-dark *text* color, so one variable can't
       do both. --accent-solid-hover is the mockup's own style-hover
       swap (a solid near-black/near-white pill goes fully black/white
       on hover) — real buttons below now use this instead of a generic
       opacity fade. */
    --accent-solid: #14181f; --accent-solid-text: #ffffff; --accent-solid-hover: #2a303b;
    --accent-soft: color-mix(in srgb, var(--ink) 8%, transparent);
    /* Status/position color system: one hue per meaning (early=blue,
       active/watch=amber, passed/support=green, failed/oppose=red),
       fixed lightness+chroma per theme so only the hue varies — ported
       directly from the mockup's own hue()/statusColor()/rag() JS
       (same H values: 245/85/145/25). --good/--error keep their
       existing names (passed/failed) since dozens of rules already
       reference them; --warn/--info are new (active-or-watch /
       introduced-or-in-committee). *-soft variants are translucent
       tints of the hue itself now, not independent flat colors, so a
       badge's soft background always stays in the same family as its
       own text/dot color. */
    --good: oklch(0.44 0.13 145); --good-soft: color-mix(in srgb, var(--good) 16%, transparent);
    --error: oklch(0.44 0.13 25); --error-soft: color-mix(in srgb, var(--error) 16%, transparent);
    --warn: oklch(0.44 0.13 85); --warn-soft: color-mix(in srgb, var(--warn) 16%, transparent);
    --info: oklch(0.44 0.13 245); --info-soft: color-mix(in srgb, var(--info) 16%, transparent);
    --shadow-rest: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03);
    --shadow-hover: 0 8px 24px rgba(0,0,0,0.10);
    /* The rest of the shadow and scrim vocabulary (whole-app audit Y31),
       so a hand-typed shadow can't drift in one rule at a time — there
       were thirteen, one copied into four menus. --shadow-pop is every
       anchored menu/dropdown; --shadow-float the dark toast and tooltip
       chips; --shadow-drawer the off-canvas sidebar; --shadow-up a bar
       or sheet pinned to the bottom (it casts upward); --ring-focus the
       soft halo on a focused field. --scrim is the one page-dimming
       overlay behind a modal, sheet, palette or drawer (it was 0.40 in
       three places and 0.45 in one). Pure black at an alpha reads the
       same on either ground, so none of these is redeclared per theme;
       in dark mode a menu's edge comes from its --rule-card border, not
       its shadow. tests/test_tokens_a11y_markup.py holds the file to
       these. */
    --shadow-pop: 0 4px 14px rgba(0,0,0,0.16);
    --shadow-float: 0 10px 30px -12px rgba(0,0,0,0.5);
    --shadow-drawer: 0 0 40px rgba(0,0,0,0.25);
    --shadow-up: 0 -4px 16px -8px rgba(0,0,0,0.4);
    --ring-focus: 0 0 0 3px color-mix(in srgb, var(--gold) 22%, transparent);
    --scrim: rgba(0,0,0,0.4);
    /* The handful of border-radius values actually used below (4-8px)
       collapse into these two tokens; 999px pills get their own, and
       --radius-lg is the mockup's card/panel/search-bar radius, now
       consumed by every card in the app: .card, .panel, .stat-card,
       .modal-panel, .current-status-card and the mobile bill card all
       used to hardcode their own radius, and all but the bill card
       hardcoded 18px, so a bill card and the panel holding it didn't
       match. One value, in one place — if 12px is the wrong call, this
       line is the whole change. Left
       alone on purpose: 50% and the two circular dots (.step-dot,
       .status-badge::before) that use a fixed pixel radius instead of
       50% only because their box is sized in rem, not px — those are
       circles, not rounded rectangles, so they're not part of this
       scale. */
    /* --radius-xs is the corner on a tiny colour swatch (a party dot, a
       vote-legend key) — 6px would turn a 7px square into a circle. */
    --radius-xs: 2px; --radius-sm: 10px; --radius-md: 12px; --radius-lg: 18px; --radius-pill: 999px;
    /* A button's corner (design system Button: 6px). Its own token, not
       a scale step, so a button and the 10px field beside it can differ
       on purpose. The whole rule, by what a thing is: anything pressed
       to act or go (buttons, links styled as buttons, back links, Undo,
       the flag toggle) takes --radius-button; anything typed into or
       picked from (fields, filter dropdowns and tabs) takes --radius-sm,
       the design system's 10px; a label or chip naming a state or topic
       (status, position, counts, chips) is a --radius-pill. */
    --radius-button: 6px;
    /* The app window's own corner (design system --radius-window): one
       box in the whole app takes it, .app-shell above 900px. */
    --radius-window: 20px;
    /* The frame (design system tokens/spacing.css): how far the window
       sits inside the canvas, the sidebar's width, a nav row's height,
       and the two shadows the frame casts — the window on the canvas and
       the raised pill under the current nav item. The design system's
       own names, kept as they are so its component specs read straight
       across; --canvas and --paper-200 alias the app's --bg / --sidebar. */
    --window-inset: 14px; --sidebar-width: 240px; --nav-item: 34px;
    --canvas: var(--bg); --paper-200: var(--sidebar);
    /* v5 dashboard names (docs/design-system tokens/colors.css), fed
       from the app's own tokens so both themes follow without a dark
       copy: the segmented track, the tint behind an Urgent triage row,
       and the tile tones (IconTile / LetterTile / DateTile). */
    --paper-50: color-mix(in srgb, var(--ink) 3%, var(--surface));
    --paper-250: color-mix(in srgb, var(--ink) 5%, var(--surface));
    --paper-350: color-mix(in srgb, var(--ink) 6%, var(--paper));
    --urgent-row: color-mix(in srgb, var(--error) 4%, var(--surface));
    --shadow-window: 0 0 0 1px rgba(20,24,31,0.06), 0 20px 50px -24px rgba(20,24,31,0.25);
    --shadow-nav-active: 0 0 0 1px var(--rule), 0 1px 3px rgba(20,24,31,0.05);
    /* The spacing scale (UX audit F). Every margin, padding and gap in
       the app is one of these — before this there were ~40 distinct
       literal values (0.35rem, 0.4rem, 1.15rem, ...) beside a six-step
       scale most rules ignored, so neighbouring panels never quite
       lined up. space-0 is the 2px hairline step (nav item stacking,
       badge nudges); 7/8 are page-level section spacing. Not redeclared
       in the dark :root — spacing doesn't change between themes.
       tests/test_design_tokens.py holds the file to this list; 0, auto,
       1px/-1px (border compensation) and clamp() are the exceptions. */
    --space-0: 2px; --space-1: 4px; --space-2: 8px; --space-3: 12px;
    --space-4: 16px; --space-5: 24px; --space-6: 32px;
    --space-7: 48px; --space-8: 64px;
    /* The type scale (UX audit F15/S7). Every font-size in the app is
       one of these — the Flagged page alone had fifteen distinct sizes,
       down to 9.9px, which erased the hierarchy they were meant to
       draw. 2xs is the floor (uppercase micro-labels, column heads,
       badges); nothing renders smaller. body stays 16px (= base), since
       inputs inherit it and anything under 16px makes iOS zoom on focus.
       tests/test_type_scale.py holds the file to this list. */
    --text-2xs: 0.75rem;   /* 12px — the floor; labels, eyebrows, meta */
    --text-xs: 0.8rem;     /* 12.8px — meta, captions, secondary lines */
    --text-sm: 0.875rem;   /* 14px   — table cells, nav, dense body */
    --text-base: 1rem;     /* 16px   — body, panel titles */
    --text-md: 1.15rem;    /* 18.4px — bill/modal titles */
    --text-lg: 1.4rem;     /* 22.4px — page h1 */
    --text-xl: 1.7rem;     /* 27.2px — stat numbers */
    --text-display: 2.2rem; /* 35.2px — dashboard highlights, sign-in h1 */
    /* Body copy is Geist (see FONT_LINKS), the mockup's declared body/UI
       face, rather than the system stack. The mockup's own body stack is
       "Geist, system-ui, sans-serif"; the fallback to system-ui keeps
       text painting if Geist hasn't loaded, and Geist self-hosts so
       nothing is fetched from a third party. --font-serif is the
       mockup's one deliberate exception — an editorial-style headline
       in Instrument Serif — and still isn't consumed by anything, so
       the webfont it named is no longer downloaded on every page load
       for nobody (see FONT_LINKS). The token stays, pointed at a
       serif every machine already has; the day a headline actually
       wants Instrument Serif it comes back as one family in FONT_LINKS
       and one word here. --mono has one job: bill numbers ("AB 1024"),
       where a fixed-width face lets a column of them line up and keeps
       "SB 1" from reading as "SB l". Nothing else is set in it — not
       counts (tabular-nums does that), not key hints, not the prose
       around a bill number, not a letter (whole-app audit Y23). */
    --font-sans: Geist, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    /* Newsreader is the display face — the headline on the landing/auth
       heroes, the page-head titles and the wordmark (see FONT_FACES in
       app.py, where its one weight is wired). It's headings only; body
       copy and small labels stay --font-sans. The stack falls through
       to a system serif so a heading keeps its shape if Newsreader
       hasn't loaded. */
    --font-display: 'Newsreader', Georgia, "Times New Roman", serif;
    --font-serif: Georgia, "Times New Roman", serif;
    --mono: 'Geist Mono', ui-monospace, monospace;
    /* One definition covers both themes — .brand-mark masks this
       shape and fills it with --ink, which already flips per theme on
       its own, so this doesn't need the redefine-in-three-places
       pattern every actual *color* above uses. */
    --brand-mark: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBkPSJNMjAgNDIgQTMwIDMwIDAgMCAxIDgwIDQyIiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMTAiLz4KICA8cmVjdCB4PSI4IiB5PSI0NyIgd2lkdGg9Ijg0IiBoZWlnaHQ9IjciLz4KICA8cmVjdCB4PSIxOCIgeT0iNTkiIHdpZHRoPSI5IiBoZWlnaHQ9IjI3Ii8+CiAgPHJlY3QgeD0iMzUiIHk9IjU5IiB3aWR0aD0iOSIgaGVpZ2h0PSIyNyIvPgogIDxyZWN0IHg9IjUyIiB5PSI1OSIgd2lkdGg9IjkiIGhlaWdodD0iMjciLz4KICA8cmVjdCB4PSI2OSIgeT0iNTkiIHdpZHRoPSI5IiBoZWlnaHQ9IjI3Ii8+CiAgPHJlY3QgeD0iMTQiIHk9IjkxIiB3aWR0aD0iNzIiIGhlaWdodD0iNyIvPgo8L3N2Zz4=");
  }
  /* :not([data-theme="light"]) so an explicit light choice (the
     manual toggle below, see account_widget()) beats the OS
     preference — without that guard, someone on a dark-mode OS who
     explicitly picks "light" here would still get this block's
     values, since a media query alone can't know about the toggle. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      /* Near-black rather than pure #000, matching the approved mockup
         (--bg #08090A, panel/card --surface #0F1011, off-white ink
         #F7F8F8). tests/test_contrast.py measures the dark ramp on
         exactly these values, read out of this file — it used to use a
         pure white-on-black stand-in, which flattered the greys (Y32). */
      --bg: #08090A; --ink: #F7F8F8; --paper: #0E0F11; --content-bg: #0E0F11; --surface: #15171A;
    --sidebar: #0B0C0E; --ink-2: #A9AFBA; --muted: #969CA8; --link: #9DB6DC;
      --gold: #C9B27A;
      --accent-solid: #F2F3F5; --accent-solid-text: #14181f; --accent-solid-hover: #D9DCE1;
      --good: oklch(0.76 0.12 145); --error: oklch(0.76 0.12 25);
      --warn: oklch(0.76 0.12 85); --info: oklch(0.76 0.12 245);
      --shadow-rest: 0 1px 3px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.3);
      --shadow-hover: 0 18px 40px rgba(0,0,0,0.35);
      --shadow-window: 0 0 0 1px rgba(255,255,255,0.05), 0 20px 50px -24px rgba(0,0,0,0.7);
      --shadow-nav-active: 0 0 0 1px var(--rule-card), 0 1px 3px rgba(0,0,0,0.4);
    }
  }
  /* The other half of the manual override: an explicit dark choice
     wins regardless of OS preference. Same values as the media-query
     block above, kept as its own always-applies rule (not folded into
     it) since [data-theme="dark"] needs to win even on a light-mode
     OS, where that media query never matches at all. */
  :root[data-theme="dark"] {
    --bg: #08090A; --ink: #F7F8F8; --paper: #0E0F11; --content-bg: #0E0F11; --surface: #15171A;
    --sidebar: #0B0C0E; --ink-2: #A9AFBA; --muted: #969CA8; --link: #9DB6DC;
    --gold: #C9B27A;
    --accent-solid: #F2F3F5; --accent-solid-text: #14181f; --accent-solid-hover: #D9DCE1;
    --good: oklch(0.76 0.12 145); --error: oklch(0.76 0.12 25);
    --warn: oklch(0.76 0.12 85); --info: oklch(0.76 0.12 245);
    --shadow-rest: 0 1px 3px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.3);
    --shadow-hover: 0 18px 40px rgba(0,0,0,0.35);
    --shadow-window: 0 0 0 1px rgba(255,255,255,0.05), 0 20px 50px -24px rgba(0,0,0,0.7);
    --shadow-nav-active: 0 0 0 1px var(--rule-card), 0 1px 3px rgba(0,0,0,0.4);
  }
  * { box-sizing: border-box; }

  /* One blanket honouring of the OS "reduce motion" setting, for the
     whole stylesheet. Before this, exactly two things respected it
     (#page-progress's transition and the toast entrance) while the
     spinner's infinite spin, .skeleton-bar's infinite opacity pulse on
     every list page, the nav caret rotation and every transition ran
     regardless — and an infinite opacity loop is the specific pattern
     that triggers vestibular discomfort.

     0.01ms rather than `none`: an animation that still runs, just
     instantly, so anything that keys off animationend (or a
     transitionend) still fires and no state machine hangs waiting.
     iteration-count:1 is what actually stops the two infinite ones.
     !important because these have to beat rules anywhere below,
     including the media-query blocks. The JS half of this lives at
     each smooth-scroll call site — see calendar_body.html,
     directory_body.html and disclosure_review_body.html. */
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
  body {
    margin: 0; background: var(--content-bg); color: var(--ink);
    font: 16px/1.5 var(--font-sans);
    -webkit-font-smoothing: antialiased;
  }
  /* A handful of plain, unclassed <a> tags (e.g. a client's name in the
     flagged-bills list) had no color rule anywhere, so they fell back to
     the browser's own default link blue instead of the app's palette —
     this is the one place that acts as a fallback for those. Anywhere
     that already sets its own color (.secondary, .primary, .panel-link,
     etc.) keeps doing so — a class selector beats this bare-tag one. */
  a { color: var(--accent); text-decoration: none; }
  /* Matches the mockup's a:hover — every plain link warms to --gold on
     hover instead of the browser default underline-only treatment. */
  @media (hover: hover) { a:hover { color: var(--gold); } }
  /* No border-radius here. This rule outranks the base .primary/
     .secondary/.filter-tab/button rules (a :focus-visible pseudo-class
     beats a bare class), so setting a radius on the *element* snapped
     every 999px pill into a 6px rectangle the instant it took keyboard
     focus — the one state only keyboard users ever see, and the one
     that looked broken. Browsers already clip `outline` to the
     element's own radius, so the declaration bought nothing. */
  a:focus-visible, button:focus-visible, input:focus-visible,
  select:focus-visible, summary:focus-visible, textarea:focus-visible,
  details:focus-visible, [tabindex]:focus-visible {
    outline: 2px solid var(--gold); outline-offset: 2px;
  }
  ::selection { background: var(--accent-solid); color: var(--accent-solid-text); }
  .wrap { max-width: 46rem; margin: 0 auto; padding: var(--space-6) var(--space-5) var(--space-8); }
  /* Auth pages (/login, /signup, /forgot, /reset, /join) — v5
     (docs/design-system: Card, forms, actions). The dome mark, a serif
     Newsreader 500 title and a one-line muted subtitle stacked above
     the form, which sits in a white Card: 18px corners on a hairline,
     no shadow. Plain paper behind it — the design system has no
     gradients and no imagery in the app, so the old gold glow and ring
     outlines are gone. Buttons and fields keep the app's own shape
     (button shape changes app-wide in one pass); field labels are the
     app's own (ink, so a label never reads as help text); links are navy and
     go to ink on hover; the checkbox is navy when ticked. */
  .auth-wrap { max-width: 26rem; padding-top: var(--space-7); text-align: center; }
  .auth-mark { margin: 0 auto var(--space-4); }
  .auth-wrap h1 {
    font-size: var(--text-display); font-weight: 500; line-height: 1.05; letter-spacing: -0.015em;
    margin: 0 0 var(--space-3); text-wrap: balance;
  }
  .auth-sub { color: var(--ink-2); font-size: var(--text-base); margin: 0 auto var(--space-6); max-width: 22rem; text-wrap: pretty; }
  /* .card.auth-card, not .auth-card: .card is defined further down the
     file, so it needs the extra class to win. */
  .card.auth-card {
    background: var(--surface); border-color: var(--rule); box-shadow: none;
    padding: var(--space-5); margin-bottom: 0; text-align: left;
  }
  .auth-card .req-legend { color: var(--ink-2); }
  .auth-submit { width: 100%; justify-content: center; margin-top: var(--space-2); }
  .auth-alt { color: var(--ink-2); font-size: var(--text-sm); margin: var(--space-5) 0 0; }
  .auth-alt a { color: var(--link); font-weight: 500; }
  @media (hover: hover) { .auth-alt a:hover, .auth-forgot:hover { color: var(--ink); } }
  .auth-wrap #loading, .auth-wrap #error { justify-content: center; margin-top: var(--space-4); }
  /* Sign In's password label with "Forgot password?" at the far end, beside the field it's about. */
  .auth-label-row { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-3); }
  .auth-forgot { display: inline-flex; align-items: center; min-height: 1.5rem; font-size: var(--text-xs); font-weight: 500; color: var(--link); }
  /* /forgot's "Check your email", in place of the form it replaces —
     the same white Card the form sat in. */
  .auth-sent {
    text-align: left; margin-top: var(--space-2); padding: var(--space-5);
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius-lg);
  }
  .auth-sent p { margin: 0; font-size: var(--text-sm); }
  .auth-sent p + p { margin-top: var(--space-2); }
  .auth-wrap .step-indicator { justify-content: center; flex-wrap: wrap; margin: 0 auto var(--space-5); }

  /* Sign-in page chrome (.auth-page): a column at least a screen tall,
     so the footer sits at the bottom of the page on a short form rather
     than just under it; the "Keep me signed in" row; the footer. */
  .auth-page { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; }
  .auth-page main { position: relative; z-index: 1; flex: 1 0 auto; }

  .auth-remember { flex: 1 1 100%; display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-0); cursor: pointer; user-select: none; font-size: var(--text-sm); color: var(--ink-2); min-height: 1.5rem; }
  .auth-remember input { width: 1.25rem; height: 1.25rem; margin: 0; flex: none; accent-color: var(--accent); cursor: pointer; }

  .auth-footer {
    position: relative; z-index: 1; display: flex; flex-wrap: wrap; align-items: center;
    justify-content: space-between; gap: var(--space-4);
    padding: var(--space-4) var(--space-5) var(--space-5);
  }
  .auth-footer-brand { font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm); color: var(--ink-2); }
  @media (max-width: 640px) {
    .auth-wrap { padding-top: var(--space-6); }
    .auth-wrap h1 { font-size: var(--text-xl); }
    .card.auth-card, .auth-sent { padding: var(--space-4); }
  }

  /* Onboarding wizard (/onboarding) — a left vertical stepper beside the
     current step. Two columns on desktop, stacked on narrow. v5
     (docs/design-system): serif Newsreader 500 titles; the current
     step a raised white pill like the sidebar's active nav item;
     field labels stay the app's own (ink, unlike help text);
     the progress label in sentence case; ticked boxes navy. */
  .onboarding {
    max-width: 58rem; margin: 0 auto; padding: var(--space-6) var(--space-5) var(--space-8);
    display: grid; grid-template-columns: 17rem 1fr; gap: var(--space-6); align-items: start;
  }
  .onboarding-mark { display: block; margin-bottom: var(--space-4); }
  .onboarding-title { font-size: var(--text-lg); font-weight: 500; line-height: 1.2; letter-spacing: -0.01em; margin: 0 0 var(--space-2); }
  .onboarding-steps .sub { margin: 0 0 var(--space-5); color: var(--ink-2); }
  .ob-stepper { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
  .ob-step { display: flex; gap: var(--space-3); align-items: flex-start; padding: var(--space-2) var(--space-2); border-radius: var(--radius-sm); }
  .ob-step.active { background: var(--surface); box-shadow: var(--shadow-nav-active); }
  .ob-num {
    flex: none; width: 1.6rem; height: 1.6rem; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: var(--text-xs); font-weight: 600; border: 1px solid var(--rule-strong); color: var(--ink-2);
  }
  .ob-step.active .ob-num { background: var(--accent-solid); color: var(--accent-solid-text); border-color: var(--accent-solid); }
  .ob-step.done .ob-num { background: var(--good-soft); color: var(--good); border-color: var(--good); }
  .ob-step.done .ob-num::after { content: "✓"; }
  .ob-step.done .ob-num { font-size: 0; }
  .ob-step.done .ob-num::after { font-size: var(--text-sm); }
  .ob-label { display: flex; flex-direction: column; line-height: 1.3; }
  .ob-label strong { font-size: var(--text-sm); font-weight: 500; }
  .ob-step:not(.active) .ob-label strong { color: var(--ink-2); }
  .ob-sub { font-size: var(--text-xs); color: var(--muted); }
  .onboarding-main { min-width: 0; }
  .onboarding-step-title { font-family: var(--font-display); font-weight: 500; font-size: var(--text-lg); line-height: 1.2; letter-spacing: -0.01em; margin: 0 0 var(--space-2); }
  .onboarding-step .sub { margin-top: 0; color: var(--ink-2); text-wrap: pretty; }
  .onboarding input[type="checkbox"] { accent-color: var(--accent); }
  .ob-field { display: block; margin-top: var(--space-4); flex: 1 1 100%; }
  .ob-field[hidden] { display: none; }   /* the display above would otherwise beat [hidden] */
  .onboarding-nav { display: flex; gap: var(--space-3); margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--rule); }
  .onboarding-nav #ob-next { margin-left: auto; }
  .ob-bill-form { display: flex; gap: var(--space-2); margin-top: var(--space-4); flex-wrap: wrap; }
  /* Step 3's title-search results: light rows from /api/search, each
     with its own Flag button. */
  .ob-results { margin-top: var(--space-4); }
  .ob-results-count { margin: 0 0 var(--space-2); }
  .ob-result-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
  .ob-result {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    padding: var(--space-2) 0; border-bottom: 1px solid var(--rule);
  }
  .ob-result-text { min-width: 0; font-size: var(--text-sm); line-height: 1.4; }
  .ob-result-text strong { font-weight: 500; font-variant-numeric: tabular-nums; margin-right: var(--space-1); }
  .ob-result-title { color: var(--ink-2); }
  .ob-result .ob-result-flag, .ob-result .ob-bill-chip-flagged { flex: none; }
  /* Step 3's clients from the firm's CAL-ACCESS filings (Oct 7 audit T3):
     a ticked client row, its filing's bills indented under it. */
  .ob-result .ob-finder-use { flex: none; white-space: nowrap; }
  .ob-filings { margin-top: var(--space-4); }
  .ob-filings-title { font-family: var(--font-display); font-weight: 500; font-size: var(--text-md); letter-spacing: -0.01em; margin: 0 0 var(--space-1); }
  .ob-filing-client { padding: var(--space-2) 0; border-bottom: 1px solid var(--rule); }
  .ob-filing-bills { list-style: none; margin: var(--space-1) 0 0; padding: 0 0 0 var(--space-5); }
  .ob-check { display: flex; gap: var(--space-2); align-items: baseline; font-size: var(--text-sm); line-height: 1.4; }
  .ob-check strong { font-weight: 500; font-variant-numeric: tabular-nums; }
  .ob-filings-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-top: var(--space-3); }
  .ob-signed-in { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--rule); font-size: var(--text-xs); color: var(--ink-2); }

  /* ── Onboarding / Create Account (2026 mockup parity) ──────────────
     The Create Account form and the three-step wizard adopt the
     mockup's visual language through the existing tokens: selectable
     radio "cards" for step 1's one question, a top progress bar on
     the wizard, and bill chips on step 3. Only the roles the app serves
     are selectable — see accounts.ONBOARDING_ROLES — so the form never
     promises a feature that isn't built. */
  .auth-wrap.auth-wide { max-width: 34rem; }

  /* Selectable radio cards — onboarding step 1's "Which describes you?"
     (role and registrant type in one answer): a real focusable
     <button> per option, a ring "dot" that fills when chosen, and the
     app's "on = accent" surface cue. */
  .role-cards { display: flex; flex-direction: column; gap: var(--space-2); }
  .role-card {
    display: grid; grid-template-columns: 1rem minmax(0, 1fr); align-items: start;
    gap: var(--space-3); width: 100%; text-align: left; cursor: pointer; font: inherit;
    padding: var(--space-3) var(--space-4); border: 1px solid var(--rule-card);
    border-radius: var(--radius-lg); background: var(--surface); color: var(--ink);
    transition: border-color .12s ease, background .12s ease;
  }
  /* Its own hover background: .role-card sits above the global
     :where(button):hover rule, which ties it on specificity and so won on
     source order — a black (light) / white (dark) fill under the card's
     ink text, the unchosen cards blanking out on hover and press. */
  @media (hover: hover) { .role-card:hover { border-color: var(--rule-strong); background: var(--surface); } }
  .role-card.selected { border-color: var(--accent-solid); background: var(--accent-soft); }
  .role-card:focus-visible { outline: 2px solid var(--accent-solid); outline-offset: 2px; }
  .role-dot {
    margin-top: var(--space-0); width: 1rem; height: 1rem; border-radius: 50%;
    border: 2px solid var(--rule-strong); flex: none; box-sizing: border-box;
  }
  .role-card.selected .role-dot {
    border-color: var(--accent-solid);
    box-shadow: inset 0 0 0 0.18rem var(--surface), inset 0 0 0 1rem var(--accent-solid);
  }
  .role-body { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
  .role-title { font-size: var(--text-sm); font-weight: 500; }
  .role-desc { font-size: var(--text-xs); line-height: 1.4; color: var(--ink-2); }
  /* On a phone the three described cards pushed the account fields
     ~1,000px down. Only the chosen card keeps its description; the rest
     are one line each, and a tap shows theirs. */
  @media (max-width: 640px) {
    .role-card:not(.selected) .role-desc { display: none; }
  }
  /* The shield "what you'll get" note, right under onboarding's choice
     it describes. flex: 1 1 100% for the same reason as .form-grid
     below: inside a flex-wrap form it needs a full-row basis. */
  .role-note {
    display: flex; gap: var(--space-2); align-items: flex-start; margin-top: var(--space-4); flex: 1 1 100%;
    padding: var(--space-3) var(--space-4); border: 1px solid var(--rule); border-radius: var(--radius-lg);
    color: var(--ink-2); font-size: var(--text-xs); line-height: 1.5;
  }
  .role-note[hidden] { display: none; }   /* the display above would otherwise beat [hidden] */
  .role-note svg { flex: none; margin-top: var(--space-0); color: var(--ink-2); }

  /* Two-up field grids (name + org; filer ID + effective date). */
  .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: var(--space-4); margin-top: var(--space-4); flex: 1 1 100%; }

  /* Wizard top progress bar. */
  .ob-progress { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-5); }
  .ob-progress-label {
    font-family: var(--font-sans); font-size: var(--text-xs); font-weight: 500;
    color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums;
  }
  .ob-progress-track { flex: 1; height: 3px; border-radius: var(--radius-pill); background: var(--accent-soft); overflow: hidden; }
  .ob-progress-fill { display: block; height: 100%; width: 33.333%; background: var(--accent-solid); border-radius: var(--radius-pill); transition: width .26s ease; }

  /* Step 3 bill chips (replacing the row list). */
  .ob-bill-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-4); }
  /* Chips: full pills, a borderless tinted fill (design system data chips). */
  .ob-bill-chip {
    display: inline-flex; align-items: center; gap: var(--space-2); height: 2rem; padding: 0 var(--space-3);
    border-radius: var(--radius-pill); border: 1px solid transparent; background: var(--paper-250);
    font-size: var(--text-xs); font-weight: 500;
  }
  .ob-bill-chip-num { font-family: var(--mono); font-weight: 400; color: var(--link); font-variant-numeric: tabular-nums; }
  .ob-bill-chip-flagged { font-size: var(--text-2xs); font-weight: 600; color: var(--good); }

  @media (max-width: 720px) {
    .onboarding { grid-template-columns: 1fr; gap: var(--space-5); }
    .ob-stepper { flex-direction: row; overflow-x: auto; gap: var(--space-2); }
    .ob-step .ob-label { display: none; }
    .ob-step.active .ob-label { display: flex; }
    .ob-signed-in { display: none; }
  }
  /* A full-width bar, same grammar as the signed-in shell's .app-topbar
     (fixed height, border-bottom, solid --paper against the page's
     --content-bg) — public pages don't get the sidebar, but the top
     chrome should still read as "this app's header," not a different
     product's thin link row. The inner div re-centers content at the
     same width as .wrap so the bar's contents still line up with the
     page below it. */
  .top-nav {
    height: 4rem; display: flex; align-items: center; background: var(--paper);
    border-bottom: 1px solid var(--rule);
  }
  .top-nav-inner {
    width: 100%; max-width: 46rem; margin: 0 auto; padding: 0 var(--space-5);
    display: flex; gap: var(--space-4); align-items: center; position: relative;
  }
  /* Wraps nav_links()/left_extra + the account widget (see top_nav()).
     display:contents at desktop widths means this introduces no box of
     its own — its children lay out directly in .top-nav-inner's flex
     row, identical to before this element existed. Only the ≤640px
     media query below turns it into an actual box (a dropdown panel). */
  .top-nav-links { display: contents; }
  /* Brand left, everything else right: the first of the page's links
     (or the account widget, on a page with none) takes the auto margin,
     so the group stays together at the far end. Desktop only — in the
     stacked mobile panel below, an auto margin would push it sideways. */
  @media (min-width: 641px) {
    .top-nav-links > :first-child { margin-left: auto; }
  }
  /* Hidden until the ≤640px media query below shows it — nothing for
     it to open above that width, since .top-nav-links is still just
     the plain inline row. Two classes (.icon-btn.top-nav-menu-btn), not
     one — .icon-btn's own `display: flex` (defined later in this file)
     would otherwise win the display property on source order alone,
     since a single-class selector here ties with it on specificity. */
  .icon-btn.top-nav-menu-btn { display: none; flex: none; margin-left: auto; }
  @media (max-width: 640px) {
    .icon-btn.top-nav-menu-btn { display: flex; }
    .top-nav-links {
      display: none; position: absolute; top: 100%; left: 0; right: 0; flex-direction: column;
      align-items: stretch; background: var(--paper); border-bottom: 1px solid var(--rule);
      padding: var(--space-4) var(--space-5); gap: var(--space-3); box-shadow: var(--shadow-pop); z-index: 30;
    }
    .top-nav-links.show { display: flex; }
    /* The account widget's own wrapper (see top_nav()'s `account`
       variable) carries margin-left:auto and a max-width inline —
       exactly what's needed to push it to the far right of a
       horizontal row, but meaningless (and, for max-width, actively
       cramping) once .top-nav-links switches to a stacked column
       above. !important earns its keep here overriding an inline
       style for a genuinely different layout context, not routine
       specificity-fighting. */
    .top-nav-links.show > div { margin-left: 0 !important; max-width: none !important; }
    /* The panel stretches its items full-width; keep the pill's label centered in it. */
    .top-nav-links.show a.secondary { justify-content: center; }
    /* /login and /signup: two links, no menu (see top_nav()). */
    .top-nav.inline-links .icon-btn.top-nav-menu-btn { display: none; }
    .top-nav.inline-links .top-nav-links {
      display: flex; position: static; flex-direction: row; align-items: center;
      margin-left: auto; padding: 0; border: 0; box-shadow: none; background: none;
    }
  }
  .top-nav a { color: var(--accent); font-size: var(--text-sm); text-decoration: none; white-space: nowrap; }
  @media (hover: hover) { .top-nav a:hover { text-decoration: underline; } }
  /* The one outlined button in the bar (/login's "Create account",
     /signup's "Sign in"). a.secondary's own `font: inherit` would otherwise size it
     at body text, a step up from the links beside it. */
  .top-nav a.secondary {
    font-size: var(--text-sm); padding: var(--space-1) var(--space-4); color: var(--ink);
  }
  @media (hover: hover) { .top-nav a.secondary:hover { text-decoration: none; } }
  /* .top-brand is a real <a> now (links home) — .top-nav a above would
     otherwise win on color/font-size since element+class ties with
     class-only on specificity's middle term; .top-nav .top-brand (two
     classes) reliably beats it instead of relying on source order. */
  .top-nav .top-brand {
    font-family: var(--font-display); font-weight: 600; font-size: var(--text-base);
    color: var(--ink); margin-right: var(--space-2);
  }
  @media (hover: hover) { .top-nav .top-brand:hover { text-decoration: none; } }
  /* Shared by every usage of the real Rotunda logo (TOP_BRAND, the
     sidebar's .app-brand, the landing page's .lp-brand and hero)
     — a sized, empty element masked to --brand-mark's shape and
     filled with --ink, which already recolors per theme on its own
     (see --brand-mark's single definition in :root). A mask, not
     background-image, so one SVG asset can serve both themes instead
     of needing separate light/dark exports. Each usage sets its own
     width/height inline, since the three contexts need different
     sizes — except inside a .brand-lockup, below. */
  .brand-mark {
    display: inline-block; flex: none; background-color: var(--ink);
    -webkit-mask-image: var(--brand-mark); mask-image: var(--brand-mark);
    -webkit-mask-size: contain; mask-size: contain;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: center; mask-position: center;
  }
  /* The mark + "Rotunda" wordmark pair (top nav, sidebar, landing nav
     and footer, auth footer). Everything is in em, so each usage sets
     only its font-size and the proportions hold at every size.
     Baseline-aligned, not centered: an empty inline-block's baseline is
     its bottom edge, so the mark's base bar sits on the text baseline
     and the dome rises past the capitals the way a round letter
     overshoots. Centering a mark taller than the caps (~0.7em in
     Newsreader) left the bar hanging below a wordmark with no
     descenders. 0.9em keeps the dome just above cap height; the 0.25em
     gap reads as ~0.32em once the artwork's own inset is counted.
     Baseline alignment packs the pair to the top of a taller box, so a
     fixed-height container (.app-brand) centers a nested lockup rather
     than being one. currentColor so the mark takes the wordmark's colour
     (the auth footer's is --slate, not --ink). The artwork is square
     (viewBox 0 0 100 100), hence aspect-ratio. The gap is named rather
     than a --space-* step because it's a ratio of the wordmark, not
     fixed px (an unregistered custom property's em resolves where it's
     used, i.e. at the wordmark). */
  .brand-lockup {
    --lockup-gap: 0.25em;
    display: inline-flex; align-items: baseline; gap: var(--lockup-gap);
  }
  .brand-lockup .brand-mark {
    height: 0.9em; width: auto; aspect-ratio: 1 / 1;
    background-color: currentColor;
  }
  h1 { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.005em; font-size: var(--text-lg); margin: 0 0 var(--space-1); }
  .sub { color: var(--slate); margin: 0 0 var(--space-6); font-size: var(--text-sm); }
  /* Every form label in the app used to be a .sub — the same treatment
     as page subtitles, help text and metadata — so on the client and
     profile forms the label and its own help text rendered identically
     and the eye couldn't separate "what this field is" from "what to
     type in it". --ink at 600 gives the label the weight; .sub stays
     what it always was, for the sentence underneath. Modelled on the
     .pref-check-text / .pref-check-sub pair further down. */
  .field-label {
    display: block;   /* also a real <label for> where a control sits beside its input */
    color: var(--ink); font-weight: 600; font-size: var(--text-xs);
    margin: 0 0 var(--space-1); letter-spacing: -0.005em;
  }
  /* Signup's own 2-step progress — two dots rather than a full wizard
     bar, since it's only ever these two steps (account, then profile
     details). Sits inline with the existing "Step 1 of 2" copy rather
     than replacing it. */
  .step-indicator { display: flex; align-items: flex-start; gap: var(--space-2); margin: 0 0 var(--space-6); }
  .step-indicator .sub { margin: var(--space-0) 0 0; }
  .step-dot { width: 0.4rem; height: 0.4rem; border-radius: 50%; background: var(--rule); flex: none; margin-top: var(--space-1); }
  .step-dot.filled { background: var(--ink); }
  form {
    display: flex; gap: var(--space-2); margin-bottom: var(--space-6); flex-wrap: wrap;
  }
  /* type="search" for the Search key on a phone keyboard, without what
     it brings along: WebKit's rounded look and a native clear × beside
     the one .search-field-clear already draws. */
  input[type="search"] { -webkit-appearance: none; appearance: none; }
  input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
  /* Fields, selects and textareas share the design system's 10px
     field corner (forms: TextInput, Select, TextArea), replacing the old
     G3 full pills (a native checkbox/radio ignores the radius). */
  input, select, textarea {
    font: inherit; padding: var(--space-2) var(--space-3); border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm); background: var(--surface); color: var(--ink);
  }
  /* The gold focus glow the mockup gives every control. Kept on :focus
     (not :focus-visible) so it shows for pointer users too; the accessible
     2px --gold keyboard outline above still lands on top for keyboard
     focus, so this only adds warmth, it doesn't replace the outline. */
  input:focus, select:focus, textarea:focus {
    border-color: var(--gold);
    box-shadow: var(--ring-focus);
  }
  select { cursor: pointer; }
  input#bill { flex: 1; min-width: 8rem; }
  /* A leading magnifier inside a text input (the mockup's search bars all
     carry one). The wrapper is relative, the icon absolutely placed, and
     the input pads left to clear it — so the field keeps its own
     gold-glow rules from above while looking like a search bar. Radius
     overridden to match .search-bar-field's rounded-rectangle shape
     (the .search-bar-field rule further down takes it to 0 instead, on
     its own higher-specificity selector, once the input sits inside
     that bigger wrapped bar rather than standing on its own). */
  .search-field { position: relative; display: flex; align-items: center; }
  .search-field-icon {
    position: absolute; left: 0.85rem; width: 0.85rem; height: 0.85rem;
    color: var(--slate); flex: none; pointer-events: none;
  }
  .search-field input { width: 100%; padding-left: var(--space-6); border-radius: var(--radius-sm); }
  /* /lookup's bill-number suggestions, hung under the whole query row
     (box and Search button) so a phone's narrow box doesn't squeeze the
     titles out. Each option is a finger-sized row: number, title (one
     line), status; on a phone the title takes a line of its own. */
  .lookup-query-form { position: relative; }
  .bill-suggest {
    position: absolute; top: calc(100% + var(--space-1)); left: 0; right: 0; z-index: 20;
    list-style: none; margin: 0; padding: var(--space-1) 0; max-height: 22rem; overflow-y: auto;
    background: var(--surface); border: 1px solid var(--rule-card); border-radius: var(--radius-md);
    box-shadow: var(--shadow-hover);
  }
  .bill-suggest[hidden] { display: none; }
  .bill-suggest-item {
    display: flex; align-items: center; gap: var(--space-2); min-height: 2.75rem;
    padding: var(--space-1) var(--space-3); cursor: pointer; font-size: var(--text-sm);
  }
  /* The keyboard's pick is stronger than the pointer's hover, so the two
     can be told apart when both are on screen. */
  .bill-suggest-item.is-active { background: var(--rule); box-shadow: inset 2px 0 0 var(--gold); }   /* see .palette-option */
  .bill-suggest-item.is-active .bill-suggest-title { color: var(--ink); }
  @media (hover: hover) { .bill-suggest-item:hover { background: var(--accent-soft); } }
  .bill-suggest-number { font-family: ui-monospace, monospace; font-weight: 600; flex: none; }
  .bill-suggest-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--slate); }
  .bill-suggest-session { flex: none; font-size: var(--text-xs); color: var(--slate); }
  @media (max-width: 767px) {
    .bill-suggest-item { flex-wrap: wrap; row-gap: 0; padding-top: var(--space-2); padding-bottom: var(--space-2); }
    .bill-suggest-item .status-badge { margin-left: auto; }
    .bill-suggest-title { order: 1; flex-basis: 100%; white-space: normal;
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
  }
  /* A trailing clear (×) control, the mirror of the leading magnifier:
     absolutely placed on the right, shown by the page only once there is
     text to clear (hidden attribute toggled in LOOKUP_BODY). The input
     pads right to make room, but only in a field that actually carries
     one — every other .search-field keeps its own spacing. */
  .search-field-clear {
    position: absolute; right: 0.5rem; display: inline-flex;
    align-items: center; justify-content: center; width: 1.5rem; height: 1.5rem;
    padding: 0; background: transparent; border: none; border-radius: var(--radius-pill);
    color: var(--slate); font-size: var(--text-md); line-height: 1; cursor: pointer;
  }
  /* display above would otherwise beat the hidden attribute, and the ×
     showed on an empty box (search/dashboard audit S10). */
  .search-field-clear[hidden] { display: none; }
  @media (hover: hover) { .search-field-clear:hover { background: var(--accent-soft); color: var(--ink); } }
  .search-field:has(.search-field-clear:not([hidden])) input { padding-right: var(--space-6); }
  /* The design system's Button: 32px tall (a 24px line plus 4px above
     and below), 6px corners, Geist 500, the ink fill with a real hover
     colour swap (--accent-solid-hover). Height comes from line-height +
     padding rather than a height/min-height, so a class that zeroes the
     padding (.linklike, .icon-btn, tabs) doesn't inherit a 32px box; a
     touch screen still gets the 44px floor (the coarse-pointer block by
     .row-menu-btn). */
  button {
    font: inherit; font-weight: 500; line-height: 1.5rem; padding: var(--space-1) var(--space-4); border: none;
    border-radius: var(--radius-button); background: var(--accent-solid); color: var(--accent-solid-text); cursor: pointer;
    transition: background .12s ease;
  }
  /* :where() zeroes this selector's specificity to (0,1,0) so a
     class-level background (a .filter-tab/.switch-btn/.linklike/… that
     opts out of the solid pill) wins on hover instead of losing to it —
     but only when that class rule comes LATER in this file: a tie goes
     to source order. A class defined above this point needs its own
     :hover background (see .role-card).
     Without that, this rule's (0,1,1) beat those (0,1,0) base rules and
     repainted transparent link/tab buttons with the solid fill while
     their text colour stayed — the invisible-on-hover text. Solid
     buttons (bare <button>, .auth-submit) set no class background, so
     they still darken. Supersedes the per-button `background:none on
     hover` band-aids noted at .settings-tab/.icon-btn. */
  @media (hover: hover) { :where(button):hover { background: var(--accent-solid-hover); } }
  button:disabled { opacity: 0.5; cursor: default; }
  /* Secondary/danger actions are outlined (transparent fill, a
     border that deepens on hover) rather than the old soft-filled
     background — matches the mockup's "Flag this bill"/"Cancel"
     treatment, where the only solid-filled button on a page is the one
     primary action. */
  button.secondary, button.danger {
    background: transparent; border: 1px solid var(--rule-strong); color: var(--ink);
  }
  /* Two pixels off the line for the border, so an outlined button is the
     same 32px as a solid one. :where() keeps this at one element's
     specificity: any class rule that sets its own line-height still wins. */
  button:where(.secondary, .danger) { line-height: calc(1.5rem - 2px); }
  button.danger { color: var(--error); }
  @media (hover: hover) { button.secondary:hover { border-color: var(--ink); } }
  @media (hover: hover) { button.danger:hover { border-color: var(--error); } }
  /* Same button look for plain <a> links used as actions (e.g. "View"
     on LegiScan, "Report" to the action-report page) — button's base
     rule only targets <button>, so links need their own copy of the
     same properties plus the anchor-specific reset. */
  a.secondary, a.danger {
    display: inline-flex; align-items: center; gap: var(--space-2); font: inherit; font-weight: 500; line-height: calc(1.5rem - 2px);
    padding: var(--space-1) var(--space-4); border-radius: var(--radius-button); text-decoration: none;
    background: transparent; border: 1px solid var(--rule-strong); color: var(--ink); transition: border-color .12s ease, color .12s ease;
  }
  a.danger { color: var(--error); }
  a.secondary svg, a.danger svg { width: 0.85rem; height: 0.85rem; flex: none; }
  /* Solid-fill counterpart to a.secondary/a.danger above, same reasoning
     — an <a> styled to look like the app's solid dark <button>. */
  a.primary {
    display: inline-flex; align-items: center; gap: var(--space-2); font: inherit; font-weight: 500; line-height: 1.5rem;
    padding: var(--space-1) var(--space-4); border-radius: var(--radius-button); text-decoration: none;
    background: var(--accent-solid); color: var(--accent-solid-text); transition: background .12s ease;
  }
  @media (hover: hover) { a.primary:hover { background: var(--accent-solid-hover); } }
  a.primary svg { width: 0.85rem; height: 0.85rem; }
  @media (hover: hover) { a.secondary:hover { border-color: var(--ink); color: var(--ink); } }
  @media (hover: hover) { a.danger:hover { border-color: var(--error); } }
  #result { display: none; }
  #result.show { display: block; }
  .card {
    background: var(--surface); border: 1px solid var(--rule-card); box-shadow: var(--shadow-rest);
    border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5); margin-bottom: var(--space-4);
  }
  .bill-id { font-family: ui-monospace, monospace; font-size: var(--text-xs); color: var(--accent); margin-bottom: var(--space-2); }
  .bill-title { font-size: var(--text-md); font-weight: 700; margin: 0 0 var(--space-1); }
  /* The report heading's number ("SB318 · Title") never breaks mid-number. */
  .bill-title-number { white-space: nowrap; }
  .bill-desc { color: var(--slate); font-size: var(--text-sm); }
  .bill-link { display: inline-block; margin-top: var(--space-2); font-size: var(--text-sm); }
  /* The one pill (UX audit F). Every static label pill — status,
     position, verdict, tag, chips, counts — shares this shape, size and
     weight; each class below only picks its colours. Before this there
     were two sizes, three paddings and three border treatments, so two
     pills side by side on one row rarely matched. The transparent 1px
     border is what keeps a bordered pill and a tinted one the same
     height; a variant that wants an outline sets border-color only.
     .tag alone stays uppercase: its text is raw CAL-ACCESS values
     ("firm", "employer") that read as labels only that way. Interactive
     chips (.filter-tab, .filter-chip, ...) are buttons, not this. */
  .status-badge, .sponsor, .tag, .position-badge, .verdict-chip, .soon-badge, .soon-tag,
  .next-chip, .due-chip, .dash-count, .party-pill, .vote-outcome, .dir-stale-badge, .dir-edit-badge, .disc-pill,
  .letter-pill {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
    padding: var(--space-0) var(--space-2); border: 1px solid transparent; border-radius: var(--radius-pill);
    font-size: var(--text-xs); font-weight: 600; line-height: 1.4; white-space: nowrap; vertical-align: middle;
  }
  /* Just a neutral dot, not color-varied by status text — LegiScan's
     status_label is a freeform string (dozens of possible values across
     bills), and guessing which ones are "good" vs "bad" well enough to
     color them confidently isn't a call this restyle should make. */
  .status-badge { background: var(--accent-soft); color: var(--accent); margin-bottom: var(--space-2); }
  .status-badge::before { content: ""; width: 0.4rem; height: 0.4rem; border-radius: var(--radius-pill); background: currentColor; flex: none; }
  /* Unlike the base rule above (deliberately neutral because LegiScan's
     own status_label is a freeform string not worth guessing at — see
     that rule's comment), a caller that owns its own small, known status
     enum (e.g. disclosure filings' draft/ready_to_file) can opt into a
     real color here instead of reaching for .position-badge, a
     different component meant for a client's stance on a bill. */
  .status-badge.good { background: var(--good-soft); color: var(--good); }
  .card-actions { margin-top: var(--space-4); display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
  /* The bill's flagged/archived state on /report — plain muted text, not
     a .status-badge pill: in a row of pill buttons, a pill reads as one
     more thing to press. Same look as search results' .flagged-mark. */
  /* On a phone Print moves into the ⋮ menu: a full button for it wrapped
     the menu onto a line of its own, and printing is rare from a phone. */
  .print-in-menu { display: none; }
  @media (max-width: 640px) {
    .card-actions .print-btn { display: none; }
    .row-menu-dropdown .print-in-menu { display: block; }
  }
  .card-actions .flagged-mark { font-size: var(--text-xs); color: var(--slate); margin-right: var(--space-1); }
  h2.section { font-size: var(--text-base); margin: var(--space-5) 0 var(--space-2); }
  table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
  td, th { padding: var(--space-2) var(--space-2); border-bottom: 1px solid var(--rule); vertical-align: top; text-align: left; }
  /* Tabular figures, not a typewriter face: the digits still line up
     down the column, and the date reads in the same type as the rest of
     the row. */
  td.date { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--slate); }
  /* Milestone left-edge on a history row (see milestoneClass() in
     LOOKUP_BODY / REPORT_BODY) — an inset box-shadow rather than a
     border, since a real border on a <td> inside a border-collapse
     table shifts column width by its own thickness; inset paints
     without taking any layout space. */
  tr.milestone-intro td:first-child { box-shadow: inset 3px 0 0 0 var(--slate); }
  tr.milestone-passed td:first-child { box-shadow: inset 3px 0 0 0 var(--good); }
  tr.milestone-amended td:first-child { box-shadow: inset 3px 0 0 0 var(--ink); }
  /* /report's "Current Status" callout (see REPORT_BODY) — the single
     most recent bill_status_history row, called out above the full
     history table below it rather than making someone read down to
     the last row to find "where things stand right now." Weight comes
     from the left-edge accent and the bold action text alone — an
     ordinary 1px card border on the plain surface. It used to be a 2px
     ink border on a tinted fill, which is how a focused or selected
     control looks, and it outweighed everything else on the page. No
     colored fill either (same restraint .status-badge's own comment
     explains: LegiScan's action text is freeform, so this isn't the
     place to guess "good" vs "bad" news).
     The left-edge accent reuses milestoneClass()'s own three
     categories (same function that colors history rows below it) so
     this card's accent means the same thing the table already
     established, not a new color vocabulary. */
  .current-status-card {
    border: 1px solid var(--rule-card); border-left: 2px solid var(--rule-strong);
    border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5);
    margin-top: var(--space-4); background: var(--surface);
  }
  .current-status-card.milestone-intro { border-left: 2px solid var(--slate); }
  .current-status-card.milestone-passed { border-left: 2px solid var(--good); }
  .current-status-card.milestone-amended { border-left: 2px solid var(--ink); }
  .current-status-card .current-status-label {
    font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--slate); margin-bottom: var(--space-2);
  }
  .current-status-card .current-status-action { font-size: var(--text-base); font-weight: 700; }
  .current-status-card .current-status-meta { font-size: var(--text-xs); color: var(--slate); margin-top: var(--space-1); }
  /* "When does this need to be amended by?" — manually entered (see
     bills.amend_by_date in schema.sql; LegiScan has no such field),
     so it lives as a small editable row inside the same callout rather
     than its own panel — one more fact about where things stand right
     now, not a separate concept. */
  /* Stacked, not a one-line flex row: this field used to be a bare
     "Amend by" label beside a date input, which is a date field on a
     compliance screen with no stated meaning — the kind users either
     avoid or fill in believing it does something. It now carries a help
     line saying what it is and where it shows up, and a save
     confirmation under the input, and those need their own lines. */
  .current-status-card .current-status-amend-by {
    margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--rule);
  }
  .current-status-card .current-status-amend-by label {
    display: block; font-size: var(--text-xs); font-weight: 600; color: var(--slate);
  }
  .current-status-card .current-status-amend-by .sub {
    font-size: var(--text-2xs); color: var(--slate); margin: var(--space-0) 0 var(--space-2);
  }
  .current-status-card .current-status-amend-by input[type="date"] {
    font-size: var(--text-xs); padding: var(--space-1) var(--space-2); width: auto;
  }
  .current-status-card .current-status-amend-by #amend-by-status {
    margin: var(--space-1) 0 0; min-height: 1em;
  }

  /* The status pill's own line on a bill report: the pill says which
     stage the bill is at, the date beside it says when that stage was
     reached. They used to run together as "Enrolled — as of 2026-08-20"
     directly above a Latest action block dated Aug 28, which read as the
     page contradicting itself rather than as two different facts. */
  .status-line { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
  .status-since { font-size: var(--text-xs); color: var(--slate); margin-bottom: var(--space-2); }
  /* A two-year bill's note beside its status word (Oct 7 audit N7). */
  .status-note { display: block; font-size: var(--text-xs); color: var(--slate); }

  /* A status the app worked out from the bill's last-action text rather
     than read from a status field — search results, where LegiScan's
     result rows carry no status code (see bill_status.js). Marked so an
     inference never sits in the same visual register as a recorded fact;
     the tooltip on the pill says so in words, since a dotted underline
     on its own is decoration to anyone who doesn't already know. */
  /* Was a dashed border; now the inference is said in the tooltip, and
     the pill itself is a solid tint plus an icon per kind of stage (the
     tones are set by STATUS_TONES in bill_status.js), so what a badge
     means never rests on its colour or its outline. */
  .status-badge.tone-early { background: var(--content-bg); color: var(--slate); border-color: var(--rule-strong); }
  .status-badge.tone-moving { background: var(--accent-soft); color: var(--accent); }
  .status-badge.tone-passed { background: var(--good-soft); color: var(--good); }
  .status-badge.tone-ended { background: var(--error-soft); color: var(--error); }
  .status-badge[class*="tone-"]::before { width: auto; height: auto; background: none; border-radius: 0; font-size: var(--text-2xs); line-height: 1; }
  .status-badge.tone-early::before { content: "\25cb"; }
  .status-badge.tone-moving::before { content: "\2192"; }
  .status-badge.tone-passed::before { content: "\2713"; }
  .status-badge.tone-ended::before { content: "\2715"; }

  /* One tooltip for every .has-tip: status badges and the Status filter
     tabs. Shown on hover AND keyboard focus (a title attribute only ever
     did the first). Positioned under its owner, above everything. */
  .has-tip { position: relative; }
  .has-tip > .tip {
    display: none; position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;
    width: max-content; max-width: 16rem; white-space: normal; text-align: left;
    font-size: var(--text-xs); font-weight: 400; line-height: 1.4; letter-spacing: 0; text-transform: none;
    background: var(--ink); color: var(--surface); border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-2); box-shadow: var(--shadow-rest); pointer-events: none;
  }
  .result-card-flags .has-tip > .tip { left: auto; right: 0; }
  /* Tap opens it too (.tip-open, tooltip.js), so hover is gated like
     every other :hover — a tap would otherwise leave it stuck open. */
  .has-tip:focus-visible > .tip, .has-tip.tip-open > .tip { display: block; }
  @media (hover: hover) { .has-tip:hover > .tip { display: block; } }
  .has-tip.tip-end > .tip { left: auto; right: 0; }
  /* An open tip takes the tap that closes it, rather than letting it
     fall through to whatever is underneath (a bill card opens its report). */
  .has-tip.tip-open > .tip { pointer-events: auto; }
  /* An open tip hangs below its badge into the rest of a phone bill
     card, whose lifted cells (z-index 1) would otherwise paint over it.
     The extra `tr` outranks those rules, which come later in the file. */
  .bill-cards tr td:has(.tip-open) { z-index: 3; }

  /* Engrossed versus enrolled is a genuine trap even for people who work
     in the building, and this app defines Firm / Employer / Coalition
     inline on Organization Search while leaving these undefined. Same
     treatment: a short glossary at the foot of any list that uses the
     pills, listing only the statuses actually on screen. */
  .status-legend {
    border-top: 1px solid var(--rule); padding: var(--space-3) var(--space-4) var(--space-4);
    display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4);
  }
  .status-legend-head {
    flex-basis: 100%; font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--slate); margin-bottom: var(--space-0);
  }
  .status-legend-item { font-size: var(--text-2xs); color: var(--slate); }
  .status-legend-term { font-weight: 700; color: var(--ink); margin-right: var(--space-1); }
  td.chamber {
    white-space: nowrap; font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase;
    color: var(--accent);
  }
  .sponsor-list { display: flex; flex-wrap: wrap; gap: var(--space-2); }
  .sponsor { background: var(--accent-soft); color: var(--accent); }
  #error {
    display: none; background: var(--error-soft); color: var(--error);
    padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); font-size: var(--text-sm); margin-bottom: var(--space-5);
  }
  #error.show { display: block; }
  /* A link in an error is the way out of it (e.g. "Add registration
     details" on Disclosures), so it reads as one: the error's own colour,
     underlined, rather than the plain-link slate that looked like text. */
  #error a { color: inherit; font-weight: 600; text-decoration: underline; }
  /* The same for a button that is the way out (the dashboard's "Try
     again"): .linklike's accent blue would sit off-colour on the banner. */
  #error .linklike { color: inherit; font-weight: 600; }
  /* An error about one field, under that field (e.g. "No California bill
     called AB 99999" under Add a bill) — #error's colour, no banner. */
  .field-error { color: var(--error); font-size: var(--text-sm); margin: var(--space-2) 0 0; }
  .field-error:empty { display: none; }
  /* The box that message is about (aria-invalid, set with the message —
     see static/js/field_errors.js). Red even while focused — focus goes
     straight to it — with the gold focus glow still around it. */
  input[aria-invalid="true"], input[aria-invalid="true"]:focus { border-color: var(--error); }
  /* Two or more problems at once: the banner lists them, left-aligned
     so a list reads as one even on the centred sign-up page. */
  #error p { margin: 0; text-align: left; }
  #error ul { margin: var(--space-1) 0 0; padding-left: var(--space-5); text-align: left; }
  /* #client-form-saving is the client modal's own "Saving…" — it can't be
     #loading because the client detail page, which shares that modal,
     already has a #loading skeleton of its own. */
  #loading, #client-form-saving { display: none; align-items: center; gap: var(--space-2); color: var(--slate); font-size: var(--text-sm); }
  #loading.show, #client-form-saving.show { display: flex; }
  #loading.skeleton.show { display: block; }
  /* Skeleton containers that can't share the #loading id — the Alerts
     page has two (digest-loading / rules-loading) — still need the same
     show/hide convention. Without these, clearing the class only strips
     `show` while the div stays a visible block full of placeholder bars,
     leaving skeletons stranded above the loaded content. */
  .skeleton { display: none; }
  .skeleton.show { display: block; }
  .spinner {
    display: inline-block; width: 0.9rem; height: 0.9rem; border-radius: 50%; flex: none;
    border: 2px solid var(--rule); border-top-color: var(--slate); animation: spin 0.6s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
  /* Organization Search's own loading state — 3 rows shaped like the
     real results table (see LOBBYING_BODY) instead of plain "Searching…"
     text, so the layout doesn't jump when real rows replace it. */
  .skeleton-row { display: flex; align-items: center; gap: var(--space-4); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--rule); }
  .skeleton-row:last-child { border-bottom: none; }
  .skeleton-bar { height: 0.8rem; border-radius: var(--radius-sm); background: var(--accent-soft); animation: skeleton-pulse 1.2s ease-in-out infinite; }
  @keyframes skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
  /* P2-32's other half: this is a plain multi-page app (see app.py's own
     module docstring — no client-side router), so every navigation is a
     real browser page load and nothing can paint DURING the network
     round trip itself. What this bar does instead: confirms the click
     landed, immediately, and keeps confirming across the gap via
     page_progress.js's sessionStorage flag, so a slow cold start (the
     3-5s the audit measured) reads as "loading" instead of "did that
     work?" — same job a native app's own progress indicator does. */
  #page-progress {
    position: fixed; top: 0; left: 0; height: 2px; width: 0%; opacity: 0;
    background: var(--accent-solid); z-index: 200; pointer-events: none;
  }
  @media (prefers-reduced-motion: reduce) { #page-progress { transition: none !important; } }
  .empty { color: var(--slate); font-size: var(--text-sm); }
  /* The mockup's zero-state: a centered dashed-border box with a muted
     icon over the message, not the bare grey line .empty gives (see
     emptyStateBox() in empty_state.js, which builds this markup). --rule-card
     (30%) is the dashed weight that reads as a box without competing with
     real content; the icon takes --slate via currentColor. */
  .empty-box {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: var(--space-3); text-align: center;
    padding: var(--space-6) var(--space-4);
    border: 1px dashed var(--rule-strong); border-radius: var(--radius-lg);
    background: var(--surface); color: var(--slate);
  }
  .empty-box svg { width: 32px; height: 32px; opacity: 0.7; flex: none; }
  .empty-box p { margin: 0; max-width: 34rem; font-size: var(--text-sm); line-height: 1.5; }
  /* The one-click next step under a zero-result message ("Search all
     sessions", "Include ended bills", "Clear all") — its own row, below
     the sentence that explains why it is offered. */
  .empty-box .empty-actions {
    display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); margin-top: var(--space-3);
  }
  /* Small centered dialog, used by the flag-confirmation modal (see
     openFlagModal() in REPORT_BODY) and the "+ Add new client"
     quick-add panel shared between it and clientCell() (see
     CLIENT_QUICKADD_JS) — the app's only two true modals, everything
     else (row menus, the account menu) is an anchored dropdown instead.
     z-index comfortably clears .row-menu-dropdown/.app-account-menu's
     own z-index: 20 above, since a modal has to sit above any dropdown
     left open behind it. */
  .modal-backdrop {
    position: fixed; inset: 0; background: var(--scrim); z-index: 100;
    display: none; align-items: center; justify-content: center; padding: var(--space-4);
  }
  .modal-backdrop.show { display: flex; }
  .modal-panel {
    background: var(--surface); border: 1px solid var(--rule-card); box-shadow: var(--shadow-hover);
    border-radius: var(--radius-lg); padding: var(--space-5); width: 100%; max-width: 26rem; max-height: 90vh; overflow-y: auto;
  }
  .modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-4); }
  .modal-head .title { font-size: var(--text-base); font-weight: 700; margin: 0; }
  .modal-head .sub { font-size: var(--text-xs); color: var(--slate); margin-top: var(--space-1); }
  .modal-panel form { margin: 0; flex-direction: column; gap: var(--space-3); }
  .modal-panel label { display: block; }
  .modal-panel input, .modal-panel select, .modal-panel textarea { width: 100%; }
  .modal-actions { display: flex; gap: var(--space-2); margin-top: var(--space-2); }
  /* The bill report's "which client is this letter for?" (D1): one row
     per assigned client — name, position, and whether a click opens
     their letter or drafts one. */
  .letter-picker-list { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-3); }
  .letter-picker-option {
    display: flex; align-items: center; gap: var(--space-2); width: 100%; text-align: left;
  }
  .letter-picker-name { flex: 1; font-weight: 600; }
  .letter-picker-action { font-size: var(--text-xs); color: var(--slate); }
  /* ── The ⌘K palette (static/js/command_palette.js) — a modal over any
     page: one field, then results grouped Bills / Clients / Letters /
     People, the full bill search last. Top-anchored rather than centred
     so the field doesn't jump as results arrive. */
  .palette-backdrop {
    position: fixed; inset: 0; background: var(--scrim); z-index: 110;
    display: flex; justify-content: center; align-items: flex-start; padding: var(--space-8) var(--space-4) var(--space-4);
  }
  .palette-backdrop[hidden] { display: none; }   /* the display above would otherwise beat [hidden] */
  body.palette-open { overflow: hidden; }
  .palette {
    width: 100%; max-width: 38rem; max-height: 70vh; display: flex; flex-direction: column;
    background: var(--surface); border: 1px solid var(--rule-card); box-shadow: var(--shadow-hover);
    border-radius: var(--radius-lg); overflow: hidden;
  }
  .palette-field {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--rule);
  }
  /* The field itself is the same rounded box as every other search in
     the app (/lookup's .search-bar-field, the .search-box filters):
     magnifier inside it, and the focus ring on the whole box. The bare
     input used to carry the global ring itself, so opening the palette
     drew a gold outline around the text alone, with the magnifier left
     outside it — unlike any other search box. */
  .palette-search {
    flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--space-2);
    min-height: 2.75rem; padding: 0 var(--space-3);
    border: 1px solid var(--rule-strong); border-radius: var(--radius-lg); background: var(--accent-soft);
  }
  .palette-search:focus-within { border-color: var(--gold); outline: 2px solid var(--gold); outline-offset: 2px; }
  .palette-search svg { width: 0.9rem; height: 0.9rem; flex: none; color: var(--slate); }
  .palette-search input {
    flex: 1; min-width: 0; border: 0; background: none; box-shadow: none;
    padding: var(--space-1) 0; font-size: var(--text-base); color: var(--ink);
  }
  /* The box carries the ring (WCAG 2.4.7; whole-app audit Y26); the
     input must not draw a second one inside it. */
  .palette-search input:focus, .palette-search input:focus-visible { outline: none; box-shadow: none; }
  .palette-search input::placeholder { color: var(--slate); }
  .palette-field kbd {
    font: inherit; font-size: var(--text-2xs); color: var(--slate);
    border: 1px solid var(--rule); border-radius: var(--radius-sm); padding: 0 var(--space-1);
  }
  .palette-list { overflow-y: auto; padding: var(--space-2) 0; }
  .palette-group + .palette-group { border-top: 1px solid var(--rule); margin-top: var(--space-1); padding-top: var(--space-1); }
  .palette-group-head {
    padding: var(--space-2) var(--space-4) var(--space-1);
    font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--slate);
  }
  .palette-option {
    display: flex; align-items: baseline; gap: var(--space-2); min-height: 2.25rem;
    padding: var(--space-2) var(--space-4); color: var(--ink); text-decoration: none; font-size: var(--text-sm);
  }
  /* The keyboard-selected option gets a gold left bar as well as the
     tint: the tint alone (1.18:1) was the same as mouse hover (Y26). */
  .palette-option[aria-selected="true"] { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--gold); }
  @media (hover: hover) { .palette-option:hover { background: var(--accent-soft); } }
  .palette-main { font-weight: 600; flex: none; max-width: 60%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .palette-sub { flex: 1; min-width: 0; color: var(--slate); font-size: var(--text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .palette-tag { flex: none; color: var(--slate); font-size: var(--text-2xs); }
  .palette-all .palette-main { max-width: none; font-weight: 500; }
  .palette-hint { margin: 0; padding: var(--space-3) var(--space-4); color: var(--slate); font-size: var(--text-sm); }
  @media (max-width: 640px) {
    .palette-backdrop { padding: var(--space-3); }
    .palette { max-height: 85vh; }
  }
  /* ── Clients page: modal add/edit + the mockup's four-column browse ──
       (see CLIENTS_BODY / clients_body.html). Two things the generic
       modal rules above don't cover:
       1. The add/edit-client form is a 14-field CAL-ACCESS record, so it
          opens in the wide panel and keeps its own multi-column row
          layout rather than .modal-panel form's single stacked column.
       2. "+ Add client" is now a modal (not the old inline expand-card),
          so the browse below it is the mockup's four data columns —
          Name / Business address / Industry / Filer ID — with no
          actions column. The row's Edit/Remove fold into one kebab
          beside the filer ID in the last cell (the same move the
          directory table made with its stale toggle), and Edit gets a
          neutral hover so it doesn't borrow the destructive-red the rest
          of a .row-menu-dropdown carries. */
  #client-modal-backdrop form { flex-direction: row; flex-wrap: wrap; }
  /* On a phone the multi-column groups (city/state/zip, amount/per) get
     too tight to read, so the form falls back to .modal-panel form's
     single stacked column below the drawer breakpoint. */
  @media (max-width: 640px) {
    #client-modal-backdrop form { flex-direction: column; }
  }
  /* Fourteen fields put "Add client" 1,700px down a 730px-tall panel on
     a phone — found only by scrolling to the end. The actions stay
     pinned to the panel's bottom edge while the fields scroll under
     them; the negative bottom offsets the panel's own padding so the
     bar sits on the edge rather than floating above a strip of fields. */
  #client-modal-backdrop .modal-actions {
    position: sticky; bottom: calc(-1 * var(--space-5)); z-index: 1;
    margin-bottom: calc(-1 * var(--space-5)); padding: var(--space-3) 0 var(--space-5);
    background: var(--surface); border-top: 1px solid var(--rule);
  }
  .clients-filer-cell { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
  .clients-filer { font-variant-numeric: tabular-nums; }
  .clients-filer-cell .row-menu { margin-left: auto; }
  /* The row's identifier outweighs what's beside it (audit F19): the
     name link was inheriting link slate while the address sat in full
     ink, so the eye landed on addresses first. */
  a.clients-name { color: var(--ink); font-weight: 600; text-decoration: none; }
  @media (hover: hover) { a.clients-name:hover { text-decoration: underline; } }
  /* The list is a portfolio (audit S9): each cell is what needs the
     reader's attention, in the Next action cell's own type — a date line,
     a grey line under it. Counts centre under their headers. */
  .clients-hearing, .clients-governor { color: var(--slate); }
  .clients-hearing-date { color: var(--ink); font-weight: 600; }
  .clients-hearing .next-when.now .clients-hearing-date { color: var(--error); }
  .clients-hearing .next-when.soon .clients-hearing-date { color: var(--warn); }
  .clients-governor .next-when { color: var(--ink); }
  .clients-governor .next-when.now { color: var(--error); }
  .clients-governor .next-when.soon { color: var(--warn); }
  .clients-count { font-variant-numeric: tabular-nums; text-align: center; }
  th.clients-count { white-space: normal; }
  .clients-interests { font-size: var(--text-xs); color: var(--slate); font-weight: 400; margin-top: var(--space-0); }
  .clients-table-wrap { overflow-x: auto; }
  /* A phone got the wide table in a sideways scroller: names wrapped to
     three lines and Edit/Remove sat in the last column, past the right
     edge. One card per client instead — name with the row menu beside
     it, then only the things that client has going on (an empty cell
     drops out, so a quiet client is a short card). The whole row already
     opens the record (link-row, row_nav.js), so nothing here needs a
     link. */
  @media (max-width: 640px) {
    .clients-table-wrap { overflow: visible; }
    .clients-table, .clients-table tbody, .clients-table td { display: block; }
    .clients-table thead { display: none; }
    .clients-table tr {
      display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--space-3);
      padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--rule-strong);
    }
    .clients-table tr:last-child { border-bottom: 0; }
    .panel .clients-table td { border: 0; padding: var(--space-0) 0; grid-column: 1; text-align: left; }
    .panel .clients-table td.clients-filer-td { grid-column: 2; grid-row: 1 / span 6; align-self: start; }
    .clients-table td.clients-hearing:empty, .clients-table td.clients-governor:empty,
    .clients-table td.clients-unseen:empty, .clients-table td.clients-open:empty { display: none; }
    .clients-table td.clients-hearing::before, .clients-table td.clients-governor::before,
    .clients-table td.clients-unseen::before, .clients-table td.clients-open::before,
    .clients-table td.clients-live::before, .clients-table .clients-filer:not(:empty)::before {
      margin-right: var(--space-1); font-size: var(--text-2xs); font-weight: 600;
      letter-spacing: 0.04em; text-transform: uppercase; color: var(--slate);
    }
    .clients-table td.clients-hearing::before { content: "Next hearing"; display: block; }
    .clients-table td.clients-governor::before { content: "Governor's desk"; display: block; }
    .clients-table td.clients-unseen::before { content: "New changes"; }
    .clients-table td.clients-open::before { content: "No position yet"; }
    .clients-table td.clients-live::before { content: "Live bills"; }
    /* The after-the-session table's three counts need their labels on a phone
       too, or a card is a name over bare numbers; a zero is blank, so its
       line is dropped rather than labelled with nothing. */
    .clients-table td.clients-signed::before { content: "Signed"; }
    .clients-table td.clients-vetoed::before { content: "Vetoed"; }
    .clients-table td.clients-died::before { content: "Died"; }
    .clients-table td.clients-signed::before, .clients-table td.clients-vetoed::before,
    .clients-table td.clients-died::before {
      margin-right: var(--space-1); font-size: var(--text-2xs); font-weight: 600;
      letter-spacing: 0.04em; text-transform: uppercase; color: var(--slate);
    }
    .clients-table td.clients-count:has(> span:empty) { display: none; }
    .clients-table .clients-filer:not(:empty)::before { content: "State filer ID"; }
    .clients-table td.clients-count, .clients-table td.clients-hearing,
    .clients-table td.clients-governor { font-size: var(--text-xs); }
  }
  .row-menu-dropdown button.row-menu-edit { color: var(--ink); }
  @media (hover: hover) { .row-menu-dropdown button.row-menu-edit:hover { background: var(--accent-soft); } }

  /* ── Client form v5 (docs/design-system: forms) ──
     The add/edit-client modal (client_form.html), shared by the Clients
     list and the client page, scoped to #client-modal-backdrop. The
     title and the two section heads in the card serif; labels medium,
     not bold; "(optional …)" asides muted. Fields and buttons keep the
     app's own shape (they change app-wide in one pass). */
  #client-modal-backdrop .modal-panel { border-color: var(--rule); }
  #client-modal-backdrop .modal-head .title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500;
    line-height: 1.2; letter-spacing: -0.01em;
  }
  #client-modal-backdrop .modal-head .sub { color: var(--ink-2); }
  #client-modal-backdrop .field-label { font-weight: 500; }
  #client-modal-backdrop .client-form-section {
    margin: var(--space-4) 0 0; font-family: var(--font-display); font-size: var(--text-md); font-weight: 500;
    line-height: 1.2; letter-spacing: -0.01em; text-transform: none;
  }
  #client-modal-backdrop .client-form-section .field-optional {
    font-family: var(--font-sans); font-size: var(--text-xs); letter-spacing: 0; color: var(--muted);
  }
  #client-modal-backdrop .field-optional { color: var(--muted); }
  /* Checkbox: the box beside its label, the hint under the label. */
  #client-modal-backdrop .client-form-check { flex: 1 1 100%; display: flex; align-items: flex-start; gap: var(--space-2); }
  #client-modal-backdrop .client-form-check input { width: auto; margin-top: var(--space-1); }
  #client-modal-backdrop .client-form-check .field-label { margin: 0; }
  #client-modal-backdrop .client-form-check .sub { display: block; margin: 0; color: var(--ink-2); }
  #client-modal-backdrop .client-form-note { flex: 1 1 100%; margin: var(--space-1) 0 0; color: var(--ink-2); text-wrap: pretty; }

  /* ── Clients list v5 (docs/design-system: Card, data, forms, actions) ──
     Laid over the rules above for the list page only (CLIENTS_BODY; the
     client record and the shared add/edit modal keep theirs). The
     two stat cards are StatTiles — a ringed icon tile beside the number,
     a hairline edge, no lift; .clients-card is a flush Card whose table
     has a tinted, rounded head band in sentence case and rows that tint
     on hover. Each client leads with its LetterTile; bill numbers are
     mono navy; the unseen count is an accent CountPill; a conflict is a
     KindPill. Buttons (Add client, the row menu) keep the app's own
     shape for now: button shape is to change app-wide in one pass. The
     --gold focus ring and the 44px touch floor stay. */

  .clients-stats { grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); gap: var(--space-3); }
  .clients-stats .stat-card {
    display: grid; grid-template-columns: 2.75rem minmax(0, 1fr); column-gap: var(--space-3); align-items: center;
    border-color: var(--rule); box-shadow: none; padding: var(--space-4);
  }
  @media (hover: hover) { .clients-stats .stat-card:hover { box-shadow: none; } }
  .clients-stats .icon-tile {
    grid-row: 1 / span 3; width: 2.75rem; height: 2.75rem; border-radius: var(--radius-md);
    box-shadow: inset 0 0 0 1px var(--rule);
  }
  .clients-stats .icon-tile svg { width: 1.125rem; height: 1.125rem; }
  .clients-stats .stat-value { letter-spacing: -0.02em; line-height: 1; }
  .clients-stats .stat-label { margin-top: var(--space-1); font-size: var(--text-sm); color: var(--ink); }
  .clients-stats .stat-foot { margin-top: var(--space-0); font-size: var(--text-2xs); color: var(--muted); }

  .panel.clients-card { border-color: var(--rule); box-shadow: none; }
  .clients-card .panel-head { border-bottom: 0; padding: var(--space-4) var(--space-4) var(--space-3); }
  .clients-card .panel-head .title {
    margin: 0; font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; letter-spacing: -0.01em;
  }
  /* The Client cell: a LetterTile, then the name over its interests. */
  .clients-ident { display: flex; align-items: flex-start; gap: var(--space-3); min-width: 0; }
  .clients-ident .letter-tile { margin-top: calc(-1 * var(--space-0)); }
  .clients-ident-text { min-width: 0; overflow-wrap: break-word; }
  .clients-card a.clients-name { font-size: var(--text-sm); font-weight: 500; color: var(--ink); }
  .clients-card .clients-interests { color: var(--ink-2); }
  .clients-card .clients-hearing, .clients-card .clients-governor { color: var(--ink-2); font-size: var(--text-xs); }
  .clients-card .clients-hearing-date, .clients-card .clients-governor .next-when { font-weight: 500; }
  .clients-card .next-where { color: var(--muted); }
  .clients-bill-number { font-family: var(--mono); font-weight: 400; color: var(--link); white-space: nowrap; }
  .clients-card td.clients-count { color: var(--ink); }
  /* CountPill, accent tone: the unseen count on the navy tint. */
  .clients-card .unread-dot {
    height: auto; min-width: 1.5rem; margin-right: 0; padding: 0 var(--space-2);
    background: var(--info-soft); color: var(--link); font-weight: 500; line-height: 1.4;
  }
  /* KindPill "conflict": a borderless red tint with a small dot. */
  .clients-card .due-chip.clients-conflict {
    min-height: 1.375rem; border-color: transparent; background: var(--error-soft); color: var(--error);
  }
  .clients-card .clients-conflict::before {
    content: ""; width: 0.3125rem; height: 0.3125rem; border-radius: var(--radius-pill); background: currentColor; flex: none;
  }
  .clients-card .clients-report a { font-weight: 500; }
  @media (min-width: 641px) {
    .clients-card .clients-table-wrap { padding: 0 var(--space-2) var(--space-2); }
    .clients-card .clients-table { border-collapse: separate; border-spacing: 0; }
    .clients-card .clients-table th {
      border-top: 0; border-bottom: 0; background: var(--paper-50); padding-top: var(--space-2); padding-bottom: var(--space-2);
      font-size: var(--text-2xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
    }
    .clients-card .clients-table th:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
    .clients-card .clients-table th:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
    .clients-card .clients-table th:first-child, .clients-card .clients-table td:first-child { padding-left: var(--space-3); }
    .clients-card .clients-table th:last-child, .clients-card .clients-table td:last-child { padding-right: var(--space-2); }
    .clients-card .clients-table tbody tr:last-child td { border-bottom: 0; }
    @media (hover: hover) {
      .clients-card .clients-table tbody tr:hover { background: none; }
      .clients-card .clients-table tbody tr:hover td { background: var(--paper-50); }
    }
  }
  /* The phone card: hairline between clients, labels in sentence case.
     The row menu sits in the card's top-right corner and the filer ID
     drops to its own line under the rest, so the name keeps the width
     the letter tile takes from it rather than sharing it with a column. */
  @media (max-width: 640px) {
    .clients-card .clients-table tr { position: relative; border-bottom-color: var(--rule); }
    .panel.clients-card .clients-table td.clients-name-td { padding-right: var(--space-6); }
    .panel.clients-card .clients-table td.clients-filer-td { grid-column: 1; grid-row: auto; }
    .clients-card .clients-filer-cell .row-menu { position: absolute; top: var(--space-2); right: var(--space-3); }
    .clients-card .clients-filer:empty { display: none; }
    /* Under the name, past the 28px tile and its 12px gap (40px). */
    .clients-card .clients-filer:not(:empty) { display: block; margin-left: var(--space-4); padding-left: var(--space-5); font-size: var(--text-xs); }
    .clients-stats .stat-card { grid-template-columns: auto minmax(0, 1fr); row-gap: 0; padding: var(--space-3); }
    .clients-stats .icon-tile { grid-row: 1; width: 2.25rem; height: 2.25rem; }
    .clients-stats .stat-label, .clients-stats .stat-foot { grid-column: 1 / -1; }
    .clients-stats .stat-label { margin-top: var(--space-2); }
    .clients-card .clients-table td::before, .clients-card .clients-table .clients-filer:not(:empty)::before {
      font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
    }
    .clients-card .clients-table td.clients-hearing, .clients-card .clients-table td.clients-governor,
    .clients-card .clients-table td.clients-count, .clients-card .clients-table td.clients-report { margin-left: var(--space-4); padding-left: var(--space-5); }
  }
  /* ── The flagged list's client cell (see billClientSummaryHtml in
       bill_clients.js and the popover wiring in FLAGGED_BODY). The list
       used to draw the full assignment editor in every row — dozens of
       controls at a handful of bills; the cell is now a read-only
       summary that opens that editor in a popover. */
  .clients-summary {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2);
    width: 100%; text-align: left; cursor: pointer; font: inherit; color: inherit;
    background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
    padding: var(--space-1) var(--space-2);
  }
  @media (hover: hover) { .clients-summary:hover { background: var(--accent-soft); border-color: var(--rule); } }
  .clients-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
  .clients-summary[aria-expanded="true"] { background: var(--accent-soft); border-color: var(--rule-strong); }
  .clients-summary-list { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); flex: 1 1 auto; }
  .client-position { display: inline-flex; align-items: center; gap: var(--space-1); }
  .client-position-name { font-size: var(--text-xs); font-weight: 600; color: var(--ink); }
  .client-position-empty { font-size: var(--text-xs); color: var(--slate); }
  /* Anchored, fixed-position editor — below .modal-backdrop's z-index so
     the "+ Add new client" quick-add modal it can open sits above it,
     and above the row menus/account menu (z-index 20) it can sit over. */
  .clients-popover {
    position: fixed; z-index: 90; width: min(26rem, calc(100vw - 1rem));
    background: var(--surface); border: 1px solid var(--rule-card);
    box-shadow: var(--shadow-hover); border-radius: var(--radius-lg);
  }
  .clients-popover[hidden] { display: none; }
  .clients-popover-head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
    padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3); border-bottom: 1px solid var(--rule);
  }
  .clients-popover-title { font-size: var(--text-sm); font-weight: 700; }
  .clients-popover-body { padding: var(--space-3) var(--space-3); max-height: min(24rem, 70vh); overflow-y: auto; }
  tr.row-link { cursor: pointer; }
  @media (hover: hover) { tr.row-link:hover { background: var(--accent-soft); } }
  .tag { text-transform: uppercase; color: var(--slate); background: var(--accent-soft); text-decoration: none; }
  /* .tag doubles as a link when it's a bill-number pill inside free
     text (see billPills() in LOBBYING_DETAIL_BODY) — everywhere else
     it's a plain, non-interactive span, so this hover rule only ever
     shows up where it's actually clickable. */
  @media (hover: hover) { a.tag:hover { background: var(--accent-solid); color: var(--accent-solid-text); } }
  /* A client's stance on a bill — same values everywhere (the
     /flagged position selector and the /report page's read-only badge),
     colored consistently: support=good, oppose=error, watch=accent. */
  /* ── Positions without relying on colour (P2-31) ──
     Red and green is the one pair a deuteranopic reader can't separate,
     and it was carrying a client's official position. Every position was
     already spelled out in words, so this was never colour ALONE — but
     reading a COLUMN of positions was a colour task, which is the state
     these badges are usually read in.

     Two non-colour cues, and they are the same two everywhere: a glyph
     (+ support, minus oppose, ring watch — matching the option text on
     the selects, see client_quickadd.js's POSITIONS), and a border, so
     the three read apart as shapes at a glance rather than as three
     tints of one shape. The border also fixes the quieter problem: a
     16% fill tint against the page is barely a boundary, so a badge
     had no edge of its own to be seen by. */
  .position-badge { border-color: currentColor; }
  .position-badge.support { background: var(--good-soft); color: var(--good); }
  .position-badge.oppose { background: var(--error-soft); color: var(--error); }
  .position-badge.watch { background: var(--accent-soft); color: var(--accent); }
  .position-badge.support::before { content: "+\00a0"; }
  .position-badge.oppose::before { content: "\2212\00a0"; }
  .position-badge.watch::before { content: "\25e6\00a0"; }
  /* The qualified positions (positions.py) take their side's colour and
     glyph — Support if amended is a support — and are told apart from the
     plain one by their words (a pill's shape is the one base rule's —
     test_design_tokens — so no edge of their own). Neutral is a stance on
     neither side: its own hue (blue, already a badge colour) and "=". */
  .position-badge.support_if_amended { background: var(--good-soft); color: var(--good); }
  .position-badge.oppose_unless_amended { background: var(--error-soft); color: var(--error); }
  .position-badge.neutral { background: var(--info-soft); color: var(--info); }
  .position-badge.support_if_amended::before { content: "+\00a0"; }
  .position-badge.oppose_unless_amended::before { content: "\2212\00a0"; }
  .position-badge.neutral::before { content: "=\00a0"; }
  /* Sponsor is a support (positions.SIDES): its side's colour and glyph. */
  .position-badge.sponsor { background: var(--good-soft); color: var(--good); }
  .position-badge.sponsor::before { content: "+\00a0"; }
  /* A stance that no longer applies: the one it was changed away from,
     and the "removed from the bill" terminus. Both are history, so
     neither carries the live colour — an Oppose that ended in June
     shouldn't be as loud on the page as the Oppose in force today. */
  .position-badge.was { background: transparent; color: var(--slate); border-color: var(--rule-strong); }
  .position-badge.removed { background: transparent; color: var(--slate); border-color: var(--rule-strong); }
  /* The selects carry their glyph in the option text itself (see
     client_quickadd.js) — ::before doesn't render inside a native
     <select>, and this is the one cue that has to show both in the
     closed control and in the open dropdown. The colour here is the
     redundant half, not the meaning. */
  select.position-select.support { border-color: var(--good); color: var(--good); }
  select.position-select.oppose { border-color: var(--error); color: var(--error); }
  select.position-select.watch { border-color: var(--accent); color: var(--accent); }
  select.position-select.support_if_amended { border-color: var(--good); color: var(--good); }
  select.position-select.oppose_unless_amended { border-color: var(--error); color: var(--error); }
  select.position-select.neutral { border-color: var(--info); color: var(--info); }
  select.position-select.sponsor { border-color: var(--good); color: var(--good); }
  /* Sponsors & votes' scorecard (P2-25): whether a BILL's own outcome —
     not any legislator's personal vote, which this app has never had —
     landed with or against one of the firm's positions on it. Always
     paired with the position-badge it's next to and spelled in words,
     same non-colour-alone rule .position-badge follows above. */
  .verdict-chip { border-color: currentColor; }
  .verdict-chip.with_us { background: var(--good-soft); color: var(--good); }
  .verdict-chip.against_us { background: var(--error-soft); color: var(--error); }
  .verdict-chip.pending { background: transparent; color: var(--slate); border-color: var(--rule-strong); }
  /* ── The client record's own facts (P2-26) ──
     Compensation, effective date, period of contract and agencies
     lobbied. The Form 601 review asked each client for these and the
     record didn't hold them, so the one workflow this app automates
     ended in hand-typing, per client, per filing.

     A definition list in all but name: label above value, wrapping into
     as many columns as fit, and a fact with no value is omitted rather
     than rendered blank — an empty value under a label reads as a
     broken field rather than as an unanswered question. */
  .client-facts {
    display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-5);
    margin-top: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--rule);
  }
  .client-facts:empty { display: none; }
  /* The header's address, interests and facts fold behind "Details" on a
     phone (audit Y10) so the bills start on the first screen; the name and
     the action buttons stay outside it. From 641px up the summary is hidden
     and the page script opens the disclosure, so the card reads as before. */
  .client-details > summary {
    width: fit-content; padding: var(--space-2) 0; margin-top: var(--space-1);
    cursor: pointer; font-size: var(--text-sm); color: var(--slate);
  }
  .client-details[open] > summary { padding-bottom: 0; }
  @media (min-width: 641px) {
    .client-details > summary { display: none; }
  }
  /* Client detail as a workspace (audit S4): the header card, then a
     tablist (.settings-tabs) whose panels are each one card. The tab
     names the panel, so a panel opens on its one-line description rather
     than a heading. The bill number is the link to its report and keeps
     the chamber cell's accent rather than turning ink. */
  .client-tabs { margin: var(--space-4) 0 var(--space-4); }
  .client-owed { margin: var(--space-4) 0 0; font-size: var(--text-sm); background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4); }
  .client-owed-head { font-size: var(--text-sm); font-weight: 500; margin: 0 0 var(--space-2); }
  .client-owed ul { margin: 0; padding-left: var(--space-5); }
  .client-owed .owed-report { color: var(--muted); }
  .client-panel-lead { margin: 0; padding: var(--space-3) var(--space-4) 0; }

  /* Sponsored measures (Oct 7 audit N5): each measure is a .saved-row
     whose five stages read as one line of small steps — a done one
     ticked, the current one in the accent — the setup strip's pattern
     (.setup-step) at row size. The forms inside a row drop the page
     form's bottom margin; they sit inside a card, not under a heading. */
  .measure-title { margin: 0; font-size: var(--text-sm); font-weight: 600; color: var(--ink); }
  .measure-bill > form, .measure-bill > div { flex: 1 1 100%; }
  .measure-steps { list-style: none; margin: var(--space-1) 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3); font-size: var(--text-2xs); color: var(--slate); }
  .measure-step.done { color: var(--ink); }
  .measure-step.current { color: var(--accent); font-weight: 700; }
  .measure-countdowns { margin: var(--space-2) 0 0; padding-left: var(--space-4); font-size: var(--text-xs); }
  .measure-bill { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-2); }
  .measure-number { font-family: var(--mono); font-weight: 600; }
  .measure-bill form, .measure-edit form { margin: var(--space-2) 0 0; }
  .measure-attach input { flex: 1; min-width: 10rem; }
  .measure-suggestions { margin: var(--space-2) 0 0; padding-left: var(--space-4); display: grid; gap: var(--space-2); }
  .measure-edit { margin-top: var(--space-2); font-size: var(--text-xs); }
  .measure-sheet:not(:empty) { margin-top: var(--space-2); display: grid; gap: var(--space-2); justify-items: start; }
  .measure-actions { display: flex; align-items: flex-start; gap: var(--space-2); flex: none; }
  /* On a phone the row's text takes the full width and the two buttons
     drop below it, rather than squeezing it into a narrow column. */
  @media (max-width: 640px) {
    .saved-row.measure { flex-wrap: wrap; }
    .measure > .saved-main { flex-basis: 100%; }
  }

  /* ── Client page v5 (docs/design-system: Card, data, forms, actions) ──
     Laid over the client-record rules above (and the "Our take" / setup
     strip ones below) for /clients/detail only, scoped to this page's own
     containers — .client-back, #client (the header card) and
     .client-workspace (the tabs and their panels) — so no other page
     moves. White cards on a hairline with no shadow; the client's name
     as a serif page title; labels and table heads in sentence case; bill
     numbers in mono navy ahead of their titles; in-row actions the
     dashboard's 30px .small. Left as they are on purpose, as on the bill
     page: the button and field shapes, the label pills' one
     base shape (test_design_tokens), and what shared scripts draw — the
     position control, conflict banners, agenda rows, the meeting log and
     position history. */
  .page-head a.client-back {
    display: inline-flex; align-items: center; gap: var(--space-1); min-height: 1.875rem;
    padding: 0 var(--space-3); border: 1px solid var(--rule-card); border-radius: var(--radius-button);
    background: var(--surface); color: var(--ink-2); font-size: var(--text-xs); font-weight: 500; white-space: nowrap;
  }
  @media (hover: hover) { .page-head a.client-back:hover { color: var(--ink); border-color: var(--rule-strong); } }

  /* The header card: the name, the address and interests as meta lines,
     the facts with sentence-case labels, the actions under a hairline. */
  #client > .card {
    display: flex; flex-direction: column; gap: var(--space-3);
    border: 1px solid var(--rule); box-shadow: none; padding: var(--space-5);
  }
  #client > .card > * { margin: 0; }
  #client .bill-title {
    font-family: var(--font-display); font-size: var(--text-display); font-weight: 500;
    line-height: 1.1; letter-spacing: -0.015em; text-wrap: balance; overflow-wrap: anywhere;
  }
  #client .client-address { margin-top: var(--space-1); font-size: var(--text-sm); color: var(--ink-2); }
  #client .client-interests {
    margin-top: var(--space-1); max-width: 54rem;
    font-size: var(--text-sm); line-height: 1.55; color: var(--ink-2); text-wrap: pretty;
  }
  #client .client-facts { margin-top: var(--space-3); padding-top: 0; border-top: 0; }
  #client .client-fact-label {
    font-size: var(--text-2xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
  }
  #client .client-fact-value { color: var(--ink); }
  #client .client-fact:first-child .client-fact-value { font-weight: 600; }
  #client .card-actions { padding-top: var(--space-3); border-top: 1px solid var(--rule); }
  #client .client-header-menu { margin-left: auto; }

  /* Tabs: the design system's underline strip — muted words, the
     current one in ink. */
  .client-workspace .client-tabs { gap: var(--space-5); }
  .client-tabs .settings-tab { font-size: var(--text-sm); font-weight: 400; color: var(--muted); }
  .client-tabs .settings-tab[aria-selected="true"] { color: var(--ink); font-weight: 500; }
  @media (hover: hover) { .client-tabs .settings-tab:hover { color: var(--ink); } }
  .client-tabs .tab-count { font-weight: 400; color: var(--muted); }

  /* Each panel is a card: hairline edge, no shadow. Its lead line, a
     body, and the add form as a footer under a hairline. */
  .client-workspace .client-panel { border: 1px solid var(--rule); box-shadow: none; }
  .client-workspace .client-panel-lead { padding: var(--space-4) var(--space-4) var(--space-1); color: var(--ink-2); }
  .client-panel-body { padding: var(--space-2) var(--space-4) var(--space-4); }
  .client-panel-foot { padding: var(--space-3) var(--space-4) var(--space-4); border-top: 1px solid var(--rule); }
  .client-workspace .client-empty { padding: var(--space-3) var(--space-4); }
  #notes-status { margin-top: var(--space-1); }
  #add-bill-loading { margin-top: var(--space-2); }
  #client-search-links { margin: var(--space-2) 0 0; }
  .client-workspace .saved-name { font-weight: 500; }

  /* The Bills table: a tinted head row in sentence case; the bill number
     in mono navy, then an ink title; a bill and its "Our take" line tint
     together on hover. */
  .client-bills th {
    background: var(--paper-50); font-size: var(--text-2xs); font-weight: 500;
    letter-spacing: 0; text-transform: none; color: var(--muted);
  }
  .client-workspace td.col-bill.chamber {
    font-family: var(--mono); font-size: var(--text-sm); font-weight: 500;
    text-transform: none; color: var(--link);
  }
  .client-bills td.col-title { font-weight: 500; color: var(--ink); }
  .client-bills td.col-author { font-size: var(--text-xs); color: var(--ink-2); }
  @media (hover: hover) {
    .client-bills tbody tr:not(.take-row):hover,
    .client-bills tbody tr:not(.take-row):hover + tr.take-row,
    .client-bills tbody tr:has(+ tr.take-row:hover),
    .client-bills tbody tr.take-row:hover { background: var(--paper-50); }
  }
  /* StatusDot: a white pill on a hairline, the word in --ink-2, the
     stage glyph alone carrying the stage's colour (as on Flagged). */
  .client-bills .status-badge[class*="tone-"] { background: var(--surface); border-color: var(--rule); color: var(--ink-2); }
  .client-bills .status-badge.tone-early::before { color: var(--muted); }
  .client-bills .status-badge.tone-moving::before { color: var(--link); }
  .client-bills .status-badge.tone-passed::before { color: var(--good); }
  .client-bills .status-badge.tone-ended::before { color: var(--error); }
  .client-bills .take-meta { color: var(--muted); }
  .client-workspace .status-legend-head { font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted); }
  .client-workspace .status-legend-term { font-weight: 600; }
  /* On a phone each bill is a card; its cell labels in sentence case.
     Two classes, to outrank the shared .bill-cards labels further down. */
  @media (max-width: 900px) {
    .bill-cards.client-bills td.col-next::before, .bill-cards.client-bills td.col-status::before,
    .bill-cards.client-bills td.col-position::before, .bill-cards.client-bills td.col-author::before {
      font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
    }
  }

  /* Contacts: the name in ink, "Primary" a sentence-case tag beside it. */
  .client-contact-name { font-size: var(--text-sm); font-weight: 500; color: var(--ink); }
  .client-contact-name .tag { margin-left: var(--space-2); text-transform: none; letter-spacing: 0; }
  /* Letters: rows inset in a tint, as on the bill page. */
  #letters .saved-row {
    padding: var(--space-2) var(--space-3); margin-bottom: var(--space-1);
    border-bottom: 0; border-radius: var(--radius-md); background: var(--paper-50);
  }
  /* Sponsored measures: the title as a row title, numbers in mono navy. */
  .client-workspace .measure-title { font-weight: 500; }
  .client-workspace .measure-number { font-weight: 500; color: var(--link); }
  .client-workspace .measure-step.current { font-weight: 600; }
  /* Saved searches: bill numbers in mono navy; "New" an accent CountPill. */
  #client-searches .digest-bill { font-weight: 500; color: var(--link); }
  #client-searches .unread-dot {
    height: auto; padding: 0 var(--space-2); background: var(--info-soft); color: var(--link);
    font-family: var(--font-sans); font-weight: 500; line-height: 1.4;
  }
  /* The setup strip: a serif card title, chips as borderless tinted
     pills, suggested bills led by their mono navy number. */
  #client-setup .setup-title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; letter-spacing: -0.01em;
  }
  #client-setup .setup-step-head h3, #client-setup .setup-num { font-weight: 600; }
  #client-setup .setup-topic { font-weight: 500; background: var(--paper-250); color: var(--ink-2); }
  #client-setup .setup-links a { font-weight: 500; }
  #client-setup .setup-bill .saved-name { font-family: var(--mono); color: var(--link); }

  @media (max-width: 640px) {
    #client > .card { padding: var(--space-4); }
    #client .bill-title { font-size: var(--text-lg); }
  }

  /* ── Client report (client_report.py) ──
     A memo, not a dashboard: one column at reading width, serif-free so
     it matches the app on screen, and on paper only the memo prints —
     the period switch, buttons and hint are app furniture, the same
     rule the position letter's print view follows. */
  .report-controls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
  #period-switch { margin: 0; }
  .report-since { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs); color: var(--slate); }
  .report-actions { margin: 0 0 0 auto; }
  /* "Mark sent" (Oct 7 audit C3): the lobbyist's record that it went out.
     App furniture like the period switch — never printed. */
  .report-sent { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); margin-top: var(--space-3); font-size: var(--text-xs); color: var(--slate); }
  .report-sent label { display: inline-flex; align-items: center; gap: var(--space-2); }
  .report-hint { margin: var(--space-2) 0 var(--space-4); }
  .client-memo { max-width: 46rem; padding: var(--space-6); line-height: 1.6; outline: none; }
  .client-memo:focus-visible { box-shadow: 0 0 0 2px var(--accent-soft); }
  .client-memo h2 { font-size: var(--text-md); margin: var(--space-6) 0 var(--space-2); }
  .client-memo .memo-head h2 { margin-top: 0; font-size: var(--text-lg); }
  .client-memo h3 { font-size: var(--text-base); margin: 0 0 var(--space-1); }
  .client-memo p { margin: 0 0 var(--space-1); }
  .memo-meta { display: grid; gap: var(--space-0); margin: var(--space-2) 0 var(--space-4); }
  .memo-meta div { display: flex; gap: var(--space-2); }
  .memo-meta dt { min-width: 3.5rem; font-weight: 600; }
  .memo-meta dd { margin: 0; }
  .memo-summary { padding-bottom: var(--space-3); border-bottom: 1px solid var(--rule-card); }
  .memo-group { padding-bottom: var(--space-1); border-bottom: 1px solid var(--rule-card); }
  .memo-bill { margin: var(--space-4) 0; break-inside: avoid; }
  .memo-activity { margin: var(--space-1) 0 var(--space-2); padding-left: var(--space-5); }
  .memo-date { font-weight: 600; white-space: nowrap; }
  .memo-quiet { color: var(--slate); font-style: italic; }
  .memo-result { margin-left: var(--space-2); font-size: var(--text-xs); font-weight: 600; white-space: nowrap; }
  .memo-result.won { color: var(--ink); }
  .memo-result.lost { color: var(--slate); }
  /* The saved parts (Oct 7 audit C2): the cover note and each bill's
     "Our take". Their indicator and an empty one's placeholder are app
     furniture — hidden on paper and when copying (.copying). */
  .memo-cover { margin: 0 0 var(--space-4); }
  .memo-take { margin-top: var(--space-2); }
  .memo-editable { white-space: pre-wrap; outline: none; border-radius: var(--radius-sm); min-height: 1.6em; }
  .memo-editable:focus-visible { box-shadow: 0 0 0 2px var(--accent-soft); }
  .memo-editable:empty::before { content: attr(data-placeholder); color: var(--slate); font-style: italic; }
  .memo-take-date { color: var(--slate); font-weight: 400; }
  .memo-take-stale { color: var(--warn); font-weight: 600; font-size: var(--text-xs); }
  .memo-ui { display: block; font-size: var(--text-xs); color: var(--slate); }
  .client-memo.copying .memo-ui, .client-memo.copying .is-empty, .client-memo.copying .memo-screen-only { display: none; }
  @media (max-width: 767px) {
    .client-memo { padding: var(--space-4); }
    .report-actions { margin-left: 0; }
  }
  @media print {
    .app-sidebar, .app-topbar, .page-head, .report-controls, .report-hint, .report-sent, #error,
    .skip-link, #page-progress, .client-memo .memo-ui, .client-memo .is-empty { display: none !important; }
    .client-memo .memo-screen-only { display: none !important; }
    .app-main { padding: 0 !important; }
    .client-memo { border: 0; box-shadow: none; padding: 0; max-width: none; color: #000; background: #fff; }
    .client-memo a { color: inherit; text-decoration: none; }
  }

  /* ── Client report v5 (docs/design-system: Card, data, forms) ──
     Laid over the rules above for /clients/report, scoped to
     .client-report-page, and screen-only: on paper the memo prints
     exactly as it did, and "Copy for email" takes markup, not these
     styles. A serif page title; the period switch a SegmentedControl
     (a paper track, the current period a white thumb); the "sent" state
     a borderless tinted chip that wraps rather than clips on a phone;
     the memo a white card on a hairline with no shadow, its bill
     numbers in mono navy and each outcome a VerdictChip. Buttons and
     fields keep the app's own shape. */
  @media screen {
    .client-report-page .page-head h1 {
      font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
      line-height: 1.05; letter-spacing: -0.015em; text-wrap: balance; overflow-wrap: anywhere;
    }
    .client-report-page .page-head #back-link { margin-bottom: var(--space-2); }
    .client-report-page #period-switch { flex-wrap: wrap; border: 0; background: var(--paper-350); }
    .client-report-page #period-switch [data-period] { color: var(--ink-2); }
    .client-report-page #period-switch [data-period][aria-current="page"] {
      background: var(--surface); color: var(--ink); font-weight: 500; box-shadow: var(--shadow-rest);
    }
    @media (hover: hover) {
      .client-report-page #period-switch [data-period]:not([aria-current]):hover { background: none; color: var(--ink); }
    }
    .client-report-page .report-since, .client-report-page .report-sent { font-weight: 500; color: var(--ink-2); }
    .client-report-page .report-sent .letter-pill { border-color: transparent; max-width: 100%; white-space: normal; text-align: left; }
    .client-report-page .report-sent .letter-pill:not(.submitted) { background: var(--paper-250); color: var(--ink-2); }
    /* A long contact name shrinks its select instead of running past the edge on a phone. */
    .client-report-page .report-sent label { max-width: 100%; min-width: 0; }
    .client-report-page .report-sent select { min-width: 0; max-width: 100%; text-overflow: ellipsis; }
    .client-report-page .report-hint { color: var(--ink-2); max-width: 46rem; text-wrap: pretty; }
    .client-report-page .client-memo { border-color: var(--rule); box-shadow: none; }
    .client-report-page .client-memo .memo-meta dt { font-weight: 500; color: var(--muted); }
    /* BillNumber: every bill the memo links to, ahead of its title. */
    .client-report-page .client-memo a[href^="/report?bill_id"] {
      font-family: var(--mono); font-size: var(--text-sm); font-weight: 500; color: var(--link); white-space: nowrap;
    }
    @media (hover: hover) { .client-report-page .client-memo a[href^="/report?bill_id"]:hover { color: var(--ink); } }
    /* VerdictChip: the outcome against the position, a tinted pill. */
    .client-report-page .memo-result {
      display: inline-flex; align-items: center; padding: 0 var(--space-2); border-radius: var(--radius-pill);
      font-weight: 500; line-height: 1.6; vertical-align: 0.1em;
    }
    .client-report-page .memo-result.won { background: var(--good-soft); color: var(--good); }
    .client-report-page .memo-result.lost { background: var(--error-soft); color: var(--error); }
    .client-report-page .memo-quiet, .client-report-page .memo-take-date,
    .client-report-page .memo-ui, .client-report-page .memo-editable:empty::before { color: var(--muted); }
  }

  /* ── Client reports list v5 (docs/design-system: Card, data) ──
     Documents > Client reports, scoped to .client-reports-page (the
     .disc-table rules are the Prepared forms list's too). A serif page
     title; the list a flush Card whose table head is a tinted, rounded
     band in sentence case and whose rows tint on hover; "Open report →"
     a link. On a phone each client is a card with its facts labelled,
     not a five-column table pushing the page sideways. */
  .client-reports-page .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em;
  }
  .client-reports-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }
  .panel.client-reports-card { border-color: var(--rule); box-shadow: none; }
  .client-reports-scroll { overflow-x: auto; padding: var(--space-2); }
  .client-reports-card .client-reports-empty { margin: 0; padding: var(--space-3) var(--space-4); }
  .client-reports-page .client-reports-table td.cr-client { font-weight: 500; color: var(--ink); }
  .client-reports-page .client-reports-table td.date { color: var(--ink-2); }
  .client-reports-page .client-reports-table td.cr-sent { white-space: normal; }
  .client-reports-page .client-reports-table .due-none { color: var(--muted); }
  .client-reports-page .client-reports-table a.disc-review-link { font-size: var(--text-sm); font-weight: 500; color: var(--link); }
  @media (hover: hover) { .client-reports-page .client-reports-table a.disc-review-link:hover { color: var(--ink); } }
  @media (min-width: 641px) {
    .client-reports-page .client-reports-table { border-collapse: separate; border-spacing: 0; }
    .client-reports-page .client-reports-table thead th {
      border-top: 0; border-bottom: 0; background: var(--paper-50);
      font-size: var(--text-xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
    }
    .client-reports-page .client-reports-table th:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
    .client-reports-page .client-reports-table th:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
    .client-reports-page .client-reports-table th:first-child, .client-reports-page .client-reports-table td:first-child { padding-left: var(--space-3); }
    .client-reports-page .client-reports-table th:last-child, .client-reports-page .client-reports-table td:last-child { padding-right: var(--space-3); }
    .client-reports-page .client-reports-table tbody tr:last-child td { border-bottom: 0; }
    @media (hover: hover) { .client-reports-page .client-reports-table tbody tr:hover td { background: var(--paper-50); } }
  }
  @media (max-width: 640px) {
    .client-reports-scroll { overflow: visible; padding: 0; }
    .client-reports-page .client-reports-table, .client-reports-page .client-reports-table tbody, .client-reports-page .client-reports-table td { display: block; }
    .client-reports-page .client-reports-table thead { display: none; }
    .client-reports-page .client-reports-table tr { display: block; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--rule); }
    .client-reports-page .client-reports-table tr:last-child { border-bottom: 0; }
    .client-reports-page .client-reports-table td, .client-reports-page .client-reports-table td:first-child, .client-reports-page .client-reports-table td:last-child {
      padding: var(--space-0) 0; border: 0; text-align: left; white-space: normal;
    }
    .client-reports-page .client-reports-table td:empty { display: none; }
    .client-reports-page .client-reports-table td.cr-client { font-size: var(--text-base); }
    .client-reports-page .client-reports-table td.cr-open { padding-top: var(--space-1); }
    .client-reports-page .client-reports-table :is(td.cr-prepared, td.cr-period, td.cr-sent)::before {
      margin-right: var(--space-1); font-size: var(--text-xs); font-weight: 500; color: var(--muted);
    }
    .client-reports-page .client-reports-table td.cr-prepared::before { content: "Last prepared"; }
    .client-reports-page .client-reports-table td.cr-period::before { content: "Period"; }
    .client-reports-page .client-reports-table td.cr-sent::before { content: "Last sent"; }
    .client-reports-page .page-head h1, .client-report-page .page-head h1 { font-size: var(--text-xl); }
  }
  /* "Our take" (Oct 7 audit C2): this client's commentary, a full-width
     line under each of their bills, joined to the bill's row above. */
  .client-bills tr:has(+ tr.take-row) > td { border-bottom: 0; }
  .client-bills tr.take-row > td { padding-top: 0; }
  .take-input { display: block; width: 100%; resize: vertical; min-height: 2.75rem; margin-top: var(--space-1); }
  .take-meta { display: flex; flex-wrap: wrap; gap: var(--space-2); min-height: 1.2em; font-size: var(--text-xs); color: var(--slate); }
  .take-when.is-stale { color: var(--warn); font-weight: 600; }
  .col-take .field-label .sub { font-weight: 400; }
  .client-workspace td.chamber a { color: inherit; text-decoration: none; }
  @media (hover: hover) { .client-workspace td.chamber a:hover { text-decoration: underline; } }
  .client-fact { display: flex; flex-direction: column; gap: var(--space-0); min-width: 8rem; }
  .client-fact-label {
    font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.04em;
    text-transform: uppercase; color: var(--slate);
  }
  /* Compensation is the one of the four a reader scans for — it's the
     number a quarterly filing reports — so it gets weight without
     getting its own colour. */
  .client-fact-value { font-size: var(--text-sm); }
  .client-fact:first-child .client-fact-value { font-weight: 700; }

  /* A new client's setup strip (workflow audit B1): three numbered steps
     in one card at the head of the Bills tab, until the client has three
     bills. A finished step keeps its content (the topics stay editable,
     the searches stay listed) and swaps its number for a tick. */
  .setup-strip { padding: var(--space-4); border-bottom: 1px solid var(--rule); }
  .setup-title { margin: 0; font-size: var(--text-base); font-weight: 700; }
  .setup-strip > .sub { margin: var(--space-1) 0 0; }
  .setup-steps { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: grid; gap: var(--space-4); }
  .setup-step-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
  .setup-step-head h3 { margin: 0; font-size: var(--text-sm); font-weight: 700; }
  .setup-num {
    display: inline-flex; align-items: center; justify-content: center; flex: none;
    width: var(--space-5); height: var(--space-5); border-radius: var(--radius-pill);
    font-size: var(--text-xs); font-weight: 700; color: var(--accent); background: var(--accent-soft);
  }
  .setup-step.done .setup-num { color: var(--accent-solid-text); background: var(--accent-solid); }
  .setup-step > :not(.setup-step-head) { margin-left: var(--space-6); }   /* the number badge plus the gap */
  .setup-hint { margin-top: var(--space-1); margin-bottom: 0; }
  .setup-topic-list { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
  .setup-topic {
    font-size: var(--text-xs); font-weight: 600; color: var(--ink);
    background: var(--content-bg); border-radius: var(--radius-pill); padding: var(--space-1) var(--space-3);
  }
  .setup-topics-form { display: flex; flex-wrap: wrap; gap: var(--space-2); }
  .setup-topics-form input { flex: 1; min-width: 12rem; }
  .setup-bills { list-style: none; margin: 0; padding: 0; }
  .setup-bill {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
    padding: var(--space-2) 0; border-bottom: 1px solid var(--rule);
  }
  .setup-bill:last-child { border-bottom: 0; }
  .setup-bill .saved-main { flex: 1 1 14rem; }
  .setup-bill-title { font-size: var(--text-sm); margin-left: var(--space-2); }
  .setup-position { font-size: var(--text-xs); }
  /* Each topic a chip, not a word in a " · " run: as inline links they
     were 16px-tall targets packed a separator apart. */
  .setup-links { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 var(--space-2); font-size: var(--text-sm); }
  .setup-links a {
    display: inline-flex; align-items: center; min-height: 1.5rem;
    padding: var(--space-1) var(--space-3); border-radius: var(--radius-pill);
    background: var(--accent-soft); color: var(--accent); font-weight: 600; text-decoration: none;
  }
  @media (hover: hover) { .setup-links a:hover { background: var(--accent-solid); color: var(--accent-solid-text); } }

  /* ── Client intake v5 (docs/design-system: Card, forms, data) ──
     /clients/intake, scoped to .client-intake-page (the setup-strip
     rules above are the client page's too). A serif page title; the
     three steps one white card on a hairline, each step's title in the
     card serif; the recorded facts as muted label + ink value; bill
     numbers in mono navy. */
  .client-intake-page .page-head a.client-back { margin-bottom: var(--space-2); }
  .client-intake-page .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em; text-wrap: balance; overflow-wrap: anywhere;
  }
  .client-intake-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }
  .client-intake-page .intake-steps {
    margin: 0 0 var(--space-4); padding: var(--space-5); gap: var(--space-5);
    border: 1px solid var(--rule); box-shadow: none;
  }
  .client-intake-page .setup-step-head { gap: var(--space-3); }
  .client-intake-page .setup-title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; line-height: 1.2; letter-spacing: -0.01em;
  }
  .client-intake-page .setup-num { font-weight: 600; }
  .client-intake-page .setup-step > :not(.setup-step-head) { margin-left: var(--space-6); }
  .client-intake-page .setup-step .sub { margin-top: var(--space-2); margin-bottom: 0; color: var(--ink-2); max-width: 46rem; text-wrap: pretty; }
  .client-intake-page .setup-step .sub:first-child { margin-top: 0; }
  .client-intake-page .setup-step .intake-fact { margin-top: var(--space-1); color: var(--ink); }
  .client-intake-page .intake-fact-label, .client-intake-page .intake-fact-missing { color: var(--muted); }
  .client-intake-page .intake-note strong { font-weight: 500; color: var(--ink); }
  .client-intake-page .intake-note a, .client-intake-page .setup-step .sub a { font-weight: 500; }
  .client-intake-page .intake-list { margin: var(--space-1) 0 0; padding-left: var(--space-5); font-size: var(--text-sm); color: var(--ink-2); }
  .client-intake-page .intake-list li { margin-top: var(--space-1); }
  .client-intake-page .intake-bill-number { font-family: var(--mono); font-weight: 500; color: var(--link); white-space: nowrap; }
  /* Checkbox: navy when ticked, the box beside its label. */
  .client-intake-page .intake-check { display: flex; align-items: flex-start; gap: var(--space-2); }
  .client-intake-page .intake-check input { width: auto; margin-top: var(--space-1); accent-color: var(--link); }
  .client-intake-page .intake-check .field-label { margin: 0; font-weight: 500; }
  @media (max-width: 640px) {
    .client-intake-page .page-head h1 { font-size: var(--text-xl); }
    .client-intake-page .intake-steps { padding: var(--space-4); }
    .client-intake-page .setup-step > :not(.setup-step-head) { margin-left: 0; }
  }

  /* One assigned client on a bill (billClientCellHtml): a small card —
     the client's name with a Remove word on the top line, and under it
     the shared position control (.position-control below), the same
     control the client record's bill table draws. It used to be a pill
     holding the dropdown with the date hanging loose underneath, which
     is why the three places a position is edited looked like three
     different things (audit S7). */
  .client-assignment {
    background: var(--content-bg); border-radius: var(--radius-md);
    padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2);
  }
  .client-chip { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-1); }
  .client-chip > a { font-size: var(--text-sm); font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
  /* The ONE position control (position_control.js): position, the date
     it has been in force since, and whether it saved — drawn the same on
     Flagged's pop-up, the bill page and the client record's bill table.
     Wraps onto a second line in a narrow column rather than squeezing. */
  .position-control { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-3); }
  .position-control select.position-select { font-size: var(--text-xs); font-weight: 600; padding: var(--space-1) var(--space-2); }
  .pc-since { display: inline-flex; align-items: center; gap: var(--space-1); }
  .pc-since label { font-size: var(--text-xs); color: var(--slate); white-space: nowrap; }
  .pc-since input[type="date"] { font-size: var(--text-xs); padding: var(--space-0) var(--space-1); width: auto; }
  .pc-status { font-size: var(--text-xs); }
  /* A finished bill's position, read-only (position_control.js endedPositionHtml, audit Y4). */
  .position-ended { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-3); font-size: var(--text-xs); }
  .position-ended .pe-since { color: var(--slate); }
  .position-ended .pe-actions { display: inline-flex; gap: var(--space-3); }
  /* Amended since this position took effect (position_control.js's
     rereviewHtml): the note, and the one-click "Still Oppose as amended".
     Its own line under the control, so it never squeezes the select. */
  .pc-rereview { flex-basis: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-3); }
  .pc-rereview .pc-reconfirm { font-size: var(--text-xs); padding: var(--space-1) var(--space-3); }
  .rereview-note { font-size: var(--text-2xs); font-weight: 600; color: var(--warn); }
  /* On Flagged's read-only cell it sits under that client's name and
     badge, so it can't push the badge into a column of its own. */
  .client-position:has(.rereview-note) { flex-wrap: wrap; }
  .client-position .rereview-note { flex-basis: 100%; }
  /* The firm on both sides of one bill (conflict_check.js, Oct 7 audit
     C1): the inline panel a save pauses on, and the banner on the bill
     page, the letter editor and the client page. Warn-toned while open;
     a recorded consent drops to a quiet note. */
  .conflict-panel, .conflict-banner {
    flex-basis: 100%; background: var(--warn-soft); border-radius: var(--radius-md);
    padding: var(--space-3) var(--space-4); margin: var(--space-2) 0;
  }
  .conflict-banner[data-status="waived"] { background: var(--content-bg); }
  .conflict-q { margin: 0 0 var(--space-1); font-size: var(--text-sm); font-weight: 600; }
  .conflict-sides, .conflict-banner p { margin: 0 0 var(--space-1); font-size: var(--text-xs); }
  .conflict-list { margin: 0 0 var(--space-1); padding-left: var(--space-5); font-size: var(--text-xs); }
  .conflict-choices { border: 0; padding: 0; margin: var(--space-2) 0; display: grid; gap: var(--space-2); }
  .conflict-choice { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); }
  .conflict-consent { display: grid; gap: var(--space-2); padding-left: var(--space-5); }
  .conflict-consent[hidden] { display: none; }
  .conflict-date { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); }
  .conflict-date label, .conflict-consent > label { font-size: var(--text-xs); }
  .conflict-date input[type="date"] { width: auto; }
  .conflict-error { margin: 0; font-size: var(--text-xs); color: var(--error); }
  .conflict-error:empty { display: none; }
  .conflict-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); margin-top: var(--space-2); }
  .clients-conflict { margin-top: var(--space-1); }
  /* A colleague saved this after it was opened here (Oct 7 audit T5):
     the letter editor's "See theirs / Keep yours". The conflict banner's
     warn tone — both are a pause before something is lost. */
  .stale-save { background: var(--warn-soft); border-radius: var(--radius-md);
                padding: var(--space-3) var(--space-4); margin: var(--space-2) 0; }
  .stale-save[hidden] { display: none; }
  .stale-save p { margin: 0 0 var(--space-2); font-size: var(--text-sm); }
  .stale-save-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
  /* Removing a client is a word, in the error colour — not a × that sat
     30px from the pop-up's own close × and looked identical to it
     (whole-app audit Y8). × now only ever means close. */
  .client-chip .chip-remove {
    background: none; border: 0; padding: var(--space-0) var(--space-2); min-height: 1.5rem;
    font: inherit; font-size: var(--text-xs); font-weight: 600; color: var(--error); cursor: pointer;
  }
  @media (hover: hover) { .client-chip .chip-remove:hover { text-decoration: underline; } }
  /* The "add a new client to this bill" control below the chips above —
     deliberately NOT styled like a position-select (those change an
     *existing* assignment; this one adds a new one), so a dashed
     outline + plain "+ Add client" wording reads as its own distinct
     affordance rather than a second, competing way to do what the chips
     above already do. */
  .add-client-select {
    font-size: var(--text-xs); font-weight: 600; color: var(--slate); background: none;
    border: 1px dashed var(--rule-strong); border-radius: var(--radius-pill); padding: var(--space-1) var(--space-2);
  }
  @media (hover: hover) { .add-client-select:hover { border-color: var(--accent); color: var(--accent); } }
  /* Filter tabs above a flagged/client-position list — same All/Support/
     Oppose/Watch vocabulary as the position badges above, just as a
     filter instead of a per-row value. */
  /* Wraps: on /alerts these rows sit inside a .panel (overflow: hidden),
     so a nowrap row didn't scroll, it was cut — at 375px "Votes" was
     wholly past the card edge and "Paused" half of it, leaving a phone
     unable to pause the digest. */
  .filter-tabs { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }
  .filter-tab {
    font: inherit; font-size: var(--text-xs); font-weight: 600; color: var(--slate);
    background: var(--surface); border: 1px solid var(--rule-strong); border-radius: var(--radius-sm);
    padding: var(--space-1) var(--space-3); cursor: pointer;
  }
  @media (hover: hover) { .filter-tab:hover { border-color: var(--ink); } }
  /* Selected is a tint, not the solid fill. The solid cream pill
     (--accent-solid) means "this does something" — Search, Flag, Save —
     and when every chosen session, filter, preset and view wore it too,
     /lookup showed ~10 identical cream shapes and the one real action
     stopped standing out. Selected state now reads from a soft tint,
     an --ink edge (vs --rule-strong at rest) and --ink text (vs
     --slate), plus the check below — so it never rests on colour
     alone. Measured on the dark page: --ink on the tint over --surface
     ≈ 16:1. The :not()s keep a zero-count / unavailable tab that
     happens to be current exactly as the disabled rules draw it. */
  .filter-tab.active {
    background: var(--accent-soft); color: var(--ink); border-color: var(--ink);
  }
  /* Hover on a selected tab can't be "border goes --ink" (it already
     is), so the tint deepens to --rule's 14% instead. */
  @media (hover: hover) {
    .filter-tab.active:not([disabled]):not([aria-disabled="true"]):hover { background: var(--rule); }
  }
  /* The count takes the tab's own colour — --slate at rest, --ink
     when pressed. It used to sit at opacity 0.7 on top of that,
     which took --slate to 3.09:1 (light) / 4.39:1 (dark) — below AA
     for a number the user is meant to read. Mono + size set it apart. */
  /* Pressed state isn't colour alone: a check leads every active tab. */
  .filter-tab.active::before { content: "\2713"; margin-right: var(--space-1); }
  .filter-tab .n { font-variant-numeric: tabular-nums; font-size: var(--text-xs); margin-left: var(--space-1); }

  /* Calendar-scoped underline tabs (Upcoming/Past). The shared .filter-tab
     above is a pill; the hearing calendar renders the same toggle as
     underline tabs per the mockup, so it uses these instead of .filter-tab.
     Kept separate so the pill styling elsewhere (lookup/alerts/flagged) is
     untouched. */
  .cal-tabs { display: flex; gap: var(--space-2); border-bottom: 1px solid var(--rule); margin-bottom: var(--space-4); }
  .cal-tab {
    font: inherit; font-size: var(--text-xs); font-weight: 500; color: var(--slate);
    background: none; border: none; border-bottom: 2px solid transparent;
    padding: var(--space-1) var(--space-3); margin-bottom: -1px; cursor: pointer;
  }
  @media (hover: hover) { .cal-tab:hover { color: var(--ink); } }
  .cal-tab.active { color: var(--ink); font-weight: 600; border-bottom-color: var(--ink); }
  .cal-tab .n { font-variant-numeric: tabular-nums; font-size: var(--text-xs); color: var(--slate); margin-left: var(--space-1); }
  /* Relative marker on a hearing group's date (Today/Tomorrow/In N days).
     Plain gold text, no pill — see the mockup; deliberately not .tag, which
     is reused for bill-number pills elsewhere. */
  .cal-when { font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--gold); }
  .page-head-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
  /* The work-calendar link panel (calendar_feed.py): numbered setup
     steps, then the private link in a read-only field beside Copy. */
  .feed-steps { margin: var(--space-3) 0 var(--space-2); padding-left: var(--space-5); font-size: var(--text-sm); }
  .feed-steps li { margin: var(--space-1) 0; }
  .feed-link { display: flex; gap: var(--space-2); align-items: center; }
  .feed-link input { flex: 1 1 auto; min-width: 0; font-size: var(--text-xs); }

  /* ── Sidebar app shell (app_shell()) ──────────────────────────────
     Only for signed-in pages, rolled out one page at a time — see
     app_shell()'s own docstring. Public pages keep the plain .wrap
     single-column layout above and just inherit the new tokens. */
  .app-shell { display: flex; min-height: 100vh; }
  .app-sidebar {
    width: var(--sidebar-width); flex: none; background: var(--sidebar); border-right: 1px solid var(--rule);
    display: flex; flex-direction: column; height: 100vh; position: sticky; top: 0;
    padding: var(--space-4) var(--space-3) var(--space-3);
  }
  /* Above the drawer width the app fills the browser edge to edge (no
     rounded window frame — the founder asked for it gone, Oct 16 2026): the sidebar is fixed in that window and .app-main is what
     scrolls. Below 900px the page goes back to scrolling as a whole —
     the drawer, the phone's collapsing address bar and the full width
     all want that, and a 14px frame round a phone screen is lost room. */
  @media (min-width: 901px) {
    body:has(.app-shell) { background: var(--canvas); }
    .app-shell {
      position: fixed; inset: 0; min-height: 0;
      overflow: hidden; background: var(--content-bg);
    }
    .app-sidebar { height: auto; position: relative; }
    .app-body { min-height: 0; }
    .app-main { overflow-y: auto; overscroll-behavior: contain; }
  }
  /* On paper the window is no frame and nothing scrolls: the page runs on. */
  @media print {
    .app-shell { position: static; overflow: visible; box-shadow: none; border-radius: 0; }
    .app-main { overflow: visible; }
  }
  /* Hidden on desktop — the hamburger next to it exists only for the
     ≤900px drawer version of the sidebar below (see that media query),
     so there's nothing for it to open until then. Two classes
     (.icon-btn.app-topbar-menu-btn), not one — .icon-btn's own
     `display: flex` (defined later in this file) would otherwise win
     the display property on source order alone, since a single-class
     selector here ties with it on specificity. */
  .icon-btn.app-topbar-menu-btn { display: none; flex: none; }
  /* Full-screen scrim behind the mobile sidebar drawer, closing it on
     click same as .row-menu-dropdown/.autofill-dropdown close on
     click-outside elsewhere in this file. Hidden/display:none outside
     the ≤900px drawer state so it never intercepts clicks on desktop,
     where the sidebar is always visible and never "open" in this sense. */
  .app-sidebar-backdrop { display: none; position: fixed; inset: 0; background: var(--scrim); z-index: 39; }
  .app-sidebar-backdrop.show { display: block; }
  /* Logo and the fold button on one row at the top of the sidebar. */
  .app-sidebar-head { display: flex; align-items: center; gap: var(--space-2); flex: none; padding: 0 0 var(--space-3) var(--space-1); }
  .app-brand {
    display: flex; align-items: center; flex: 1; min-width: 0; min-height: 2rem;
    font-family: var(--font-display); font-weight: 600;
    font-size: var(--text-base); letter-spacing: -0.005em;
    color: var(--ink); text-decoration: none;
  }
  /* No square badge — just the mark itself, colored like the rest of
     the app's text (--ink), sitting directly on --paper. Light mode:
     dark mark on white. Dark mode: white mark on black. */
  /* The nav scrolls on a short window; the head, search and foot don't. */
  nav.app-nav { flex: 1; min-height: 0; overflow-y: auto; }
  .app-nav ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-0); }
  /* "Workspace" / "Library": small muted sentence case, not caps. */
  .side-nav-group {
    margin: 0; padding: var(--space-3) var(--space-2) var(--space-1);
    font-size: var(--text-2xs); font-weight: 500; color: var(--muted);
  }
  .side-nav-item {
    height: var(--nav-item); display: flex; align-items: center; gap: var(--space-2); border-radius: var(--radius-sm); padding: 0 var(--space-2);
    font-size: var(--text-sm); font-weight: 500; color: var(--ink-2); text-decoration: none;
    transition: background .12s ease, color .12s ease;
  }
  .side-nav-item svg { width: 1rem; height: 1rem; flex: none; stroke-width: 1.3; }
  /* Hover tints only once the pointer has really moved on this page
     (app_shell's script drops .nav-pointer-idle on the first
     pointermove). A freshly loaded page otherwise inherits whatever
     item sits under a resting cursor (after Back, a keyboard or palette
     jump, a scripted browser) and paints it grey beside the real
     current item. The current item never takes the hover tint. */
  @media (hover: hover) { .app-shell:not(.nav-pointer-idle) .side-nav-item:not(.active):hover { background: var(--accent-soft); color: var(--ink); } }
  /* "You are here" is more than the hover tint — a raised white pill
     with its own edge, and bold — so hovering another item never makes
     two look current (WCAG 1.4.1; whole-app audit Y27). */
  .side-nav-item.active { background: var(--surface); color: var(--ink); font-weight: 600; box-shadow: var(--shadow-nav-active); }
  /* Alerts' unseen-change count (app_shell's script): an ink pill, a
     number, with "new" said to a screen reader, so the colour carries
     nothing alone. */
  .side-nav-badge {
    display: inline-block; margin-left: auto; min-width: 1.25rem; padding: 0 var(--space-1);
    border-radius: var(--radius-pill); background: var(--accent-solid); color: var(--accent-solid-text);
    font-size: var(--text-2xs); font-weight: 600; line-height: 1.25rem; text-align: center;
  }
  .side-nav-badge[hidden] { display: none; }
  /* The fold button under the nav (app_shell()'s script). Styled as one
     more sidebar row, quieter than the destinations above it. Desktop
     only: below 900px the sidebar is a drawer and has nothing to fold. */
  /* An icon button beside the logo; its words are for screen readers
     and its tooltip. */
  .sidebar-collapse-btn {
    display: flex; align-items: center; justify-content: center; flex: none;
    width: 1.75rem; height: 1.75rem; padding: 0;
    border: none; border-radius: var(--radius-sm); background: none; cursor: pointer;
    font: inherit; color: var(--muted);
  }
  .sidebar-collapse-btn .side-nav-text {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  .sidebar-collapse-btn svg { width: 0.95rem; height: 0.95rem; flex: none; transition: transform .15s ease; }
  @media (hover: hover) { .sidebar-collapse-btn:hover { background: var(--accent-soft); color: var(--ink); } }
  @media (max-width: 900px) { .sidebar-collapse-btn { display: none; } }
  /* Folded: a 3.5rem rail of icons. The names stay in the links, only
     visually hidden (the .sr-only recipe), so each link keeps its
     accessible name; the tooltip shows it on hover and focus. */
  @media (min-width: 901px) {
    .app-sidebar { transition: width .15s ease; }
    .sidebar-collapsed .app-sidebar { width: 3.5rem; padding-left: var(--space-2); padding-right: var(--space-2); }
    .sidebar-collapsed .side-nav-text, .sidebar-collapsed .brand-name,
    .sidebar-collapsed .app-account-text, .sidebar-collapsed .app-account > svg,
    .sidebar-collapsed .side-nav-group, .sidebar-collapsed .topbar-search kbd {
      position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
      overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
    }
    /* Folded, the head stacks: the mark, then the unfold button. */
    .sidebar-collapsed .app-sidebar-head { flex-direction: column; padding: 0 0 var(--space-2); }
    .sidebar-collapsed .app-brand { justify-content: center; flex: none; }
    .sidebar-collapsed .side-nav-item, .sidebar-collapsed .topbar-search { justify-content: center; padding: 0; gap: 0; }
    /* The group labels fold away; a gap keeps the two groups apart. */
    .sidebar-collapsed .app-nav ul + .side-nav-group + ul { margin-top: var(--space-3); }
    .sidebar-collapsed .sidebar-collapse-btn svg { transform: scaleX(-1); }
    /* Folded to icons, the badge sits on the icon's corner. */
    .sidebar-collapsed .side-nav-item { position: relative; }
    .sidebar-collapsed .side-nav-badge { position: absolute; top: 0; right: 0; min-width: 1rem; line-height: 1rem; }
    .sidebar-collapsed .app-account { justify-content: center; padding: var(--space-2) 0; }
    /* The menu is wider than the rail, so it opens out over the page. */
    .sidebar-collapsed .app-account-menu { right: auto; left: var(--space-2); min-width: 12rem; }
  }
  /* Settings and the account card, pinned to the bottom of the sidebar. */
  .app-sidebar-foot { padding: var(--space-3) 0 0; flex: none; position: relative; display: flex; flex-direction: column; gap: var(--space-0); }
  .app-sidebar-foot .app-account-guest, .app-sidebar-foot .app-account { margin-top: var(--space-1); border-top: 1px solid var(--rule); }
  .app-sidebar-foot .app-account-guest { padding-top: var(--space-2); }
  .app-account {
    display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-2); border-radius: var(--radius-sm);
    cursor: pointer; transition: background .12s ease; background: none; border: none; width: 100%;
    text-align: left; color: var(--ink); /* button's own base rule sets color: var(--accent-solid-text) —
    white-on-white in light mode, dark-on-dark in dark — this overrides it back to the real text color. */
  }
  @media (hover: hover) { .app-account:hover { background: var(--accent-soft); } }
  .app-avatar {
    height: 1.75rem; width: 1.75rem; border-radius: var(--radius-pill); background: var(--ink); color: var(--paper);
    font-size: var(--text-2xs); font-weight: 600; display: flex; align-items: center; justify-content: center; flex: none;
  }
  .app-account-email {
    font-size: var(--text-xs); font-weight: 500; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; flex: 1; min-width: 0;
  }
  /* The sidebar card's two lines: the person, then the firm. */
  .app-account-text { display: flex; flex-direction: column; flex: 1; min-width: 0; line-height: 1.3; }
  .app-account-text .app-account-email { flex: none; }
  .app-account-org {
    font-size: var(--text-2xs); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .app-account-org:empty { display: none; }
  .app-account-chevron { color: var(--muted); flex: none; }
  .app-account-menu {
    position: absolute; left: 0; right: 0; bottom: 3.75rem; background: var(--surface);
    border: 1px solid var(--rule-card); border-radius: var(--radius-md); padding: var(--space-2); box-shadow: var(--shadow-pop);
    display: none; flex-direction: column; gap: var(--space-1); z-index: 20;
  }
  /* account_widget()'s dropdown on a public page (top_nav()'s call
     passes this) sits in a top bar, not a bottom-anchored sidebar
     footer — opens downward from the account button instead of
     upward, and isn't stretched to a sidebar-footer's own width. */
  .app-account-menu.top-anchored { left: auto; right: 0; bottom: auto; top: 100%; margin-top: var(--space-2); min-width: 11rem; }
  .app-account-menu.show { display: flex; }
  .app-account-menu button, .app-account-menu a {
    display: block; background: none; border: none; color: var(--ink); font: inherit; font-weight: 500; font-size: var(--text-xs);
    text-align: left; padding: var(--space-2) var(--space-2); border-radius: var(--radius-sm); cursor: pointer; text-decoration: none;
  }
  @media (hover: hover) { .app-account-menu button:hover, .app-account-menu a:hover { background: var(--accent-soft); } }
  /* Settings, on /profile — the "you are here" cue the sidebar item used to carry. */
  .app-account-menu a[aria-current="page"] { background: var(--accent-soft); }
  /* The dark-mode toggle row inside account_widget()'s dropdown, right
     above Sign out. .app-account-menu button already sets display:
     block for a plain full-width label — this needs display:flex
     instead to lay the "Dark mode" label and the switch out side by
     side, hence the extra class in the selector for enough specificity
     to actually win over that rule rather than just losing quietly. */
  .app-account-menu button.theme-toggle-row {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); cursor: pointer;
  }
  /* The off track also carries a --rule-strong edge: --rule alone is
     1.45:1 against a dark card, so "off" read as "missing" (WCAG 1.4.11
     needs 3:1 for the part that shows state; whole-app audit R5). An
     inset shadow, not a border, so the knob's position doesn't shift. */
  .theme-toggle-track {
    position: relative; width: 1.9rem; height: 1.05rem; border-radius: var(--radius-pill);
    background: var(--rule); box-shadow: inset 0 0 0 1px var(--rule-strong);
    flex: none; transition: background .15s ease;
  }
  .theme-toggle-row[aria-checked="true"] .theme-toggle-track { background: var(--ink); }
  .theme-toggle-thumb {
    position: absolute; top: 0.1rem; left: 0.1rem; width: 0.85rem; height: 0.85rem;
    border-radius: 50%; background: var(--surface); transition: transform .15s ease;
  }
  .theme-toggle-row[aria-checked="true"] .theme-toggle-thumb { transform: translateX(0.85rem); }
  /* Shown instead of the avatar/email button when /api/me says no one's
     signed in — /lookup and /lobbying are the two pages that render this
     shell without requiring a session (see the module docstring's "no
     account needed to look up a bill" promise), so the sidebar footer
     needs an honest logged-out state rather than a blank avatar next to
     a "Sign out" button that wouldn't do anything. */
  /* Which of these two the footer shows is decided by the server now
     (see app.py's _fill_account_state), through data-account on both
     halves, rather than by JS unhiding one after /api/me answered. An
     unfilled page shows neither — the same empty footer it used to
     show for the length of that fetch, rather than a raw token. */
  .app-account-guest { display: none; gap: var(--space-2); }
  .app-account-guest[data-account="guest"] { display: flex; }
  .app-account[data-account="guest"] { display: none; }
  /* white-space: nowrap matters once this renders inside top_nav()'s
     own width-capped wrapper (see .app-account-menu.top-anchored's own
     comment) — without it, "Sign up" wrapped onto its own line inside
     its own button instead of the two flex:1 buttons just sizing down
     evenly, since nothing was forcing the browser to treat the label's
     natural single-line width as this button's minimum. */
  .app-account-guest a { flex: 1; justify-content: center; text-align: center; padding: var(--space-2) 0; font-size: var(--text-xs); white-space: nowrap; }
  /* top_nav()'s call (account_widget(..., guest_plain=True)) — undoes
     the sidebar's button-pair treatment above for two plain, unclassed
     <a> tags instead, which already pick up .top-nav a's color/size/
     underline-on-hover on their own. Matches how "Sign in"/"Sign up"
     always looked on a public page before this shared component
     existed — two solid pill buttons read as too heavy sitting next
     to the rest of the top bar's plain text links. */
  .app-account-guest.plain-links a { flex: none; padding: 0; font-size: var(--text-sm); }
  .app-body { flex: 1; display: flex; flex-direction: column; min-width: 0; }
  .app-topbar {
    height: 4rem; flex: none; display: flex; align-items: center; justify-content: space-between;
    padding: 0 var(--space-5); border-bottom: 1px solid var(--rule); background: var(--paper); gap: var(--space-4);
  }
  /* Inside the window the topbar is only the date, as a quiet meta line
     over the page's own header — no bar, no rule (the design system's
     PageHeader puts the date in its meta). The hamburger row comes back
     with the drawer below 900px. */
  @media (min-width: 901px) {
    .app-topbar { height: auto; border-bottom: 0; background: none; padding: var(--space-4) var(--space-5) 0; }
  }
  .app-topbar-sub { font-size: var(--text-xs); color: var(--slate); }
  .app-topbar-lead { display: flex; align-items: center; gap: var(--space-4); min-width: 0; }
  /* A phone's topbar names the page, not the date (app_shell's script
     fills it from the tab title). Wider screens keep the date. */
  .app-topbar-title { display: none; }
  @media (max-width: 640px) {
    .app-topbar-lead .app-topbar-sub { display: none; }
    .app-topbar-title {
      display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
      font-size: var(--text-sm); font-weight: 600; color: var(--ink);
    }
    /* Held back while the page's own h1 is on screen, so the name isn't
       said twice at the top of the page (dashboard audit Q7). */
    .app-topbar-title.waits { visibility: hidden; }
    .app-topbar-title.waits.shown { visibility: visible; }
  }
  /* The way into search from every other page. There WAS a global
     search input here once, removed as redundant — /lookup is one
     sidebar click away and does the same job better, so a second
     always-visible box competing with it was furniture. Still true,
     which is why this is a link shaped like a search field and not a
     field: it navigates rather than searching in place, and its real
     job is to say that ⌘K exists (see search_shortcut.js). Sized and
     coloured off .search-box below so the two read as the same family
     without one pretending to be the other. */
  /* At the top of the sidebar now: a white field-shaped link. */
  .topbar-search {
    display: flex; align-items: center; gap: var(--space-2); height: var(--nav-item); border-radius: var(--radius-sm); flex: none;
    background: var(--surface); border: 1px solid var(--rule-card); box-shadow: var(--shadow-rest);
    padding: 0 var(--space-2); font-size: var(--text-sm); color: var(--muted);
    text-decoration: none; white-space: nowrap;
  }
  .topbar-search .side-nav-text { flex: 1; }
  @media (hover: hover) { .topbar-search:hover { border-color: var(--ink); color: var(--ink); } }
  /* On the search page itself: lit like the current nav item (Oct 19 audit). */
  .topbar-search.active { border-color: var(--ink); color: var(--ink); font-weight: 600; }
  .topbar-search svg { width: 0.85rem; height: 0.85rem; flex: none; }
  .topbar-search kbd {
    font: inherit; font-size: var(--text-2xs);
    border: 1px solid var(--rule); border-radius: var(--radius-xs);
    padding: 0 var(--space-1); color: var(--muted);
  }
  /* The key hint only means something with a keyboard: gone on phones
     and on touch-first devices, where the magnifier link still works. */
  @media (max-width: 767px), (hover: none), (pointer: coarse) {
    .topbar-search kbd { display: none; }
  }

  /* Was shared topbar chrome on every page (a global "search LegiScan"
     box + a persistent "Flag a bill" shortcut) until both were removed
     as redundant — see .topbar-search above for what came back and how
     it differs. What follows is FLAGGED_BODY's own local
     filter-as-you-type box for the list already loaded on that page —
     a different job (filtering, not searching LegiScan), which is why
     it survived while the shared version didn't. */
  .search-box {
    display: flex; align-items: center; gap: var(--space-2); height: 2rem; width: 12.5rem; border-radius: var(--radius-lg);
    background: var(--accent-soft); border: 1px solid var(--rule-strong); padding: 0 var(--space-3); font-size: var(--text-xs); color: var(--slate);
  }
  /* Same gold focus glow the bare inputs get, but the real <input> here is
     borderless inside the pill, so the glow rides the wrapper via
     :focus-within. */
  .search-box:focus-within { border-color: var(--gold); outline: 2px solid var(--gold); outline-offset: 2px; }
  .search-box svg { width: 0.75rem; height: 0.75rem; flex: none; }
  .search-box input {
    background: none; border: none; outline: none; color: var(--ink); font-size: var(--text-xs);
    width: 100%; font-family: inherit;
  }
  /* The pill carries the focus ring; the borderless input must not draw
     a second one inside it (global input:focus glow / :focus-visible). */
  .search-box input:focus, .search-box input:focus-visible,
  .dir-search-box input:focus, .dir-search-box input:focus-visible { outline: none; box-shadow: none; }
  .search-box input::placeholder { color: var(--slate); }
  /* color belongs here and not only on the svg child below: button's
     own base rule paints text var(--accent-solid-text), which in dark
     mode is near-black — so an .icon-btn holding a text glyph rather
     than an icon (the × on a saved search, and now on a client row of
     the 601 review) came out black on black and simply wasn't there. */
  .icon-btn {
    height: 2rem; width: 2rem; border-radius: var(--radius-md); border: 1px solid var(--rule-strong); background: var(--paper);
    color: var(--slate);
    display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background .12s ease;
  }
  @media (hover: hover) { .icon-btn:hover { background: var(--accent-soft); } }
  /* flex: none matters here, not just as a style preference — without
     it this is the one svg-in-a-flex-row rule in the whole file that
     omits it (compare .side-nav-item/.search-box/.frame-nav-item svg
     above), and the default flex-shrink:1 collapses an <svg> with no
     width/height HTML attributes down to 0 width in a flex container,
     even with an explicit CSS width set. Confirmed via
     getComputedStyle() — this button's bell icon was genuinely
     rendering at 0x14px, not just a design nitpick. */
  .icon-btn svg { width: 0.85rem; height: 0.85rem; color: var(--slate); flex: none; }
  /* Used to be `display: none` below 700px — FLAGGED_BODY renders this
     inside a `display:flex;flex-wrap:wrap` panel-head row alongside the
     "Sort by" select (see that call site), so instead of removing the
     filter-as-you-type box outright (leaving mobile users with no way
     to filter their flagged list at all), give it width:100% — the
     flex-wrap parent already drops it to its own full-width row once it
     no longer fits next to Sort-by, same as any other wrapped flex
     item. */
  @media (max-width: 700px) { .search-box { width: 100%; } }
  /* The document scrolls, not .app-main: .app-shell is min-height, so
     this box grows with its content. It was overflow-y:auto anyway, and
     a scroll container that never scrolls pins nothing — every sticky
     child (the Settings rail and Save bar, /lookup's search bar, the 601
     section nav and actions) scrolled away, and each page patched it
     back with its own :has() override. `clip` still stops content
     spilling sideways without making this a scroll container. */
  .app-main { flex: 1; overflow-x: clip; padding: var(--space-5); background: var(--content-bg); }
  /* The page's own big heading + description + right-aligned controls
     (e.g. filter tabs), living inside .app-main — distinct from the
     small, generic "Overview" label in .app-topbar above it. */
  .page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-4); flex-wrap: wrap; }
  .page-head h1 { font-family: var(--font-display); font-size: var(--text-lg); font-weight: 600; letter-spacing: -0.005em; margin: 0; }
  .page-head .sub { font-size: var(--text-xs); color: var(--slate); margin: var(--space-0) 0 0; }
  /* The "← Flagged bills" back link is small text on its own line, so its
     box was ~15px tall, under WCAG 2.5.8's 24px (audit F24). min-height
     grows the target without enlarging the text. */
  .page-head a.sub { display: inline-flex; align-items: center; min-height: 1.5rem; }
  /* A page's caveats, folded under its one-line description (Y24):
     "More about this data" opens them in place. The summary keeps its
     disclosure triangle (so it reads as something that opens) and its
     padding makes it a 24px target, not a 15px line of small text. */
  .page-head-more { margin-top: var(--space-1); max-width: 46rem; }
  .page-head-more > summary {
    width: fit-content; padding: var(--space-1) 0; line-height: 1.4;
    cursor: pointer; font-size: var(--text-xs); color: var(--slate);
  }
  .page-head-more > .sub { margin-top: var(--space-1); }
  .page-head .filter-tabs { margin-bottom: 0; }
  /* Settings sub-tabs (the mockup's 4-way Settings shell on /profile).
     An underline tablist: the active tab carries an --ink underline and
     full-strength text, the rest sit at --slate. --rule-strong on the
     resting underline track so the row of tabs reads as controls, not
     decoration (the same 3:1 reasoning as every other control edge). */
  .settings-tabs {
    display: flex; gap: var(--space-4); border-bottom: 1px solid var(--rule);
    margin: 0 0 var(--space-4); overflow-x: auto;
  }
  /* On a phone the report's and client record's tab rows are wider than
     the screen, and the only sign more tabs existed was a thin scrollbar.
     Scroll shadows, CSS only: a shade sits at each edge that has hidden
     tabs beyond it (attachment: scroll), and a page-coloured cover that
     travels with the content (attachment: local) hides it once that end
     is reached. Nothing shows when the row fits — the cover is solid for
     the shade's full 2.5rem; solid for only part of it, the rest of the
     shade showed through as grey blocks on a row that fit. */
  @media (max-width: 900px) {
    .settings-tabs {
      background:
        linear-gradient(to right, var(--content-bg) 2.5rem, transparent) left / 3.5rem 100% no-repeat local,
        linear-gradient(to left, var(--content-bg) 2.5rem, transparent) right / 3.5rem 100% no-repeat local,
        linear-gradient(to right, color-mix(in srgb, var(--ink) 30%, transparent), transparent) left / 2.5rem 100% no-repeat scroll,
        linear-gradient(to left, color-mix(in srgb, var(--ink) 30%, transparent), transparent) right / 2.5rem 100% no-repeat scroll;
      scrollbar-width: none;
    }
    .settings-tabs::-webkit-scrollbar { display: none; }
  }
  /* A page's own views strip can outrun a laptop too (Compliance has
     nine), where it cut "Prepared forms" off mid-word with no sign of
     more. The same shadows there; the page starts the strip scrolled to
     its current tab (_area_views_strip). */
  @media (min-width: 901px) {
    .area-views {
      background:
        linear-gradient(to right, var(--content-bg) 2.5rem, transparent) left / 3.5rem 100% no-repeat local,
        linear-gradient(to left, var(--content-bg) 2.5rem, transparent) right / 3.5rem 100% no-repeat local,
        linear-gradient(to right, color-mix(in srgb, var(--ink) 30%, transparent), transparent) left / 2.5rem 100% no-repeat scroll,
        linear-gradient(to left, color-mix(in srgb, var(--ink) 30%, transparent), transparent) right / 2.5rem 100% no-repeat scroll;
      scrollbar-width: none;
    }
    .area-views::-webkit-scrollbar { display: none; }
  }
  .settings-tab {
    background: none; border: none; border-bottom: 2px solid transparent;
    padding: var(--space-2) var(--space-0); margin-bottom: -1px; color: var(--slate);
    font: inherit; font-size: var(--text-sm); font-weight: 600; white-space: nowrap;
    cursor: pointer; border-radius: 0;
  }
  /* background:none on hover too, or the base button:hover (white fill)
     wins the background cascade and paints near-white text on white. */
  @media (hover: hover) { .settings-tab:hover { color: var(--ink); background: none; } }
  .settings-tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--ink); background: none; }
  /* A count beside a tab's label ("Bills 3") — quieter than the label
     so the row still reads as words. Empty (a zero) takes no space. */
  .tab-count { margin-left: var(--space-0); font-weight: 500; color: var(--slate); }
  .tab-count:empty { display: none; }
  /* The same strip as links between pages (/flagged's four views — see
     flagged_views_nav in app.py): aria-current marks the page you're on. */
  a.settings-tab { display: inline-flex; align-items: center; text-decoration: none; }
  a.settings-tab[aria-current="page"] { color: var(--ink); border-bottom-color: var(--ink); }
  /* The Bills area's scope switch (audit S4): Tracking | All California
     bills, a segmented control at the head of every page in the area.
     Two options in one track, not two tabs — it changes which set of
     bills the page is about, where the underline strip below it only
     changes the view of one set, so it must not borrow that strip's
     underline. It is drawn the way the app already draws "you are
     here": the chosen option is the sidebar's active row (.side-nav-item
     .active) — --accent-soft tint, --ink text, weight 600 — sitting in a
     rounded track, with a --rule-strong edge so it reads as a shape and
     not from colour alone. Never the cream fill that means "this does
     something". Sizes follow the sidebar and the filter pills beside it
     (--radius-md track, --text-xs), not the page heading's. */
  .scope-switch {
    display: inline-flex; gap: var(--space-0); padding: var(--space-0);
    margin: 0 0 var(--space-3); max-width: 100%;
    background: var(--surface); border: 1px solid var(--rule-strong); border-radius: var(--radius-md);
  }
  .scope-option {
    display: inline-flex; align-items: center; min-height: 1.75rem; padding: 0 var(--space-3);
    border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: 500; color: var(--slate);
    text-decoration: none; white-space: nowrap; transition: background .12s ease, color .12s ease;
  }
  @media (hover: hover) { .scope-option:hover { color: var(--ink); background: var(--accent-soft); } }
  .scope-option[aria-current="page"] {
    color: var(--ink); font-weight: 600; background: var(--accent-soft);
    box-shadow: inset 0 0 0 1px var(--rule-strong);
  }
  /* A touch target the same height as the sidebar rows on a phone. */
  @media (max-width: 900px) { .scope-option { min-height: 2.5rem; } }
  .soon-badge { margin-left: var(--space-2); border-color: var(--rule-strong); color: var(--slate); }
  /* The inert 2FA/devices panels read as "not yet" from their Coming
     soon badge and disabled button — not from fading the description,
     which is text the user is meant to read. */
  .settings-soon button[disabled] { cursor: not-allowed; }

  /* ── /profile Settings shell (vertical sub-nav, mockup parity) ───────
     The mockup rebuilds Settings as a header row (avatar-initial + name)
     over a 220px left rail and an inline-editable body. Distinct classes
     from .settings-tabs above on purpose: those stay the horizontal
     underline tablist that /report and /alerts reuse — this page is the
     only one with the side rail. */
  .settings-head { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-5); }
  .settings-avatar {
    flex: none; width: 2.75rem; height: 2.75rem; border-radius: 50%;
    background: var(--accent-solid); color: var(--accent-solid-text);
    display: flex; align-items: center; justify-content: center;
    font-family: var(--font-sans); font-size: var(--text-md); font-weight: 700;
  }
  .settings-id { min-width: 0; }
  .settings-id h1 {
    font-family: var(--font-display); font-size: var(--text-lg); font-weight: 600;
    letter-spacing: -0.005em; margin: 0;
  }
  .settings-id .sub { font-size: var(--text-xs); color: var(--slate); margin: var(--space-0) 0 0; }
  .settings-head-actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-3); }

  .settings-layout {
    display: grid; grid-template-columns: 220px minmax(0, 1fr);
    gap: clamp(var(--space-5), 4vw, var(--space-7)); align-items: start;
  }
  .settings-nav { display: flex; flex-direction: column; gap: var(--space-0); position: sticky; top: var(--space-4); }
  .settings-nav-item {
    display: flex; align-items: center; height: 2.25rem; padding: 0 var(--space-3);
    background: none; border: none; border-radius: var(--radius-md);
    color: var(--slate); font: inherit; font-size: var(--text-sm); font-weight: 600;
    text-align: left; cursor: pointer;
  }
  @media (hover: hover) { .settings-nav-item:hover { color: var(--ink); background: var(--accent-soft); } }
  .settings-nav-item[aria-selected="true"] { color: var(--ink); background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--gold); }   /* see .side-nav-item.active */
  .settings-content { min-width: 0; }
  .settings-lead { margin-bottom: var(--space-4); }
  .settings-lead-title { font-size: var(--text-base); font-weight: 700; letter-spacing: -0.01em; margin: 0; }
  .settings-lead .sub { font-size: var(--text-xs); color: var(--slate); margin: var(--space-1) 0 0; }

  /* Inline-editable field grid (Profile + Org tabs). Plain <label>s, not
     inside a <form>, so they don't inherit form's flex-row wrap. */
  .settings-fields {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: var(--space-4); margin-bottom: var(--space-4);
  }
  .settings-field { display: flex; flex-direction: column; gap: var(--space-1); }
  .settings-field .field-label { margin: 0; }
  .settings-field-wide { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-4); }
  .settings-field-wide .field-label { margin: 0; }
  /* A field that takes the whole row of the grid — the street line in the
     Firm tab's address (workflow audit C2). */
  .settings-field-span { grid-column: 1 / -1; }
  .settings-fields[hidden] { display: none; }   /* the display above would otherwise beat [hidden] */
  .field-hint { font-size: var(--text-xs); color: var(--slate); }
  .settings-field input[readonly] { background: var(--accent-soft); color: var(--slate); cursor: default; }
  .settings-field input[readonly]:focus { border-color: var(--rule-strong); box-shadow: none; }

  @media (max-width: 640px) {
    .settings-layout { grid-template-columns: 1fr; gap: var(--space-4); }
    .settings-nav { position: static; flex-direction: row; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; gap: var(--space-1); }
    .settings-nav::-webkit-scrollbar { display: none; }
    /* Same edge-shade pattern as .settings-tabs above: a shade at each end
       with hidden tabs beyond it, covered once that end is reached. */
    .settings-nav {
      background:
        linear-gradient(to right, var(--content-bg) 2.5rem, transparent) left / 3.5rem 100% no-repeat local,
        linear-gradient(to left, var(--content-bg) 2.5rem, transparent) right / 3.5rem 100% no-repeat local,
        linear-gradient(to right, color-mix(in srgb, var(--ink) 30%, transparent), transparent) left / 2.5rem 100% no-repeat scroll,
        linear-gradient(to left, color-mix(in srgb, var(--ink) 30%, transparent), transparent) right / 2.5rem 100% no-repeat scroll;
    }
    .settings-nav-item { flex: none; white-space: nowrap; }
    /* Pinned to the bottom of the screen: as a row in the header it was
       off-screen while you edited anything below the fold. It used to
       hold a Save button; now it holds the autosave status, so it shows
       only while there is something to say. Below the drawer (40). */
    .settings-head-actions {
      position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; margin: 0;
      justify-content: flex-end; padding: var(--space-3) var(--space-4);
      background: var(--surface); border-top: 1px solid var(--rule);
    }
    .settings-head-actions:not(:has(.save-status:not([hidden]):not([data-state="idle"]))) { display: none; }
    .settings-layout { padding-bottom: var(--space-8); }
  }

  /* ── Settings v5 (docs/design-system: Card, forms, data) ──
     Laid over the /profile shell above, scoped to .profile-page
     (PROFILE_BODY) since .panel, .saved-row, .field-label, .field-hint,
     the .pref-* digest rows and .soon-badge are shared with other pages.
     The user's name as a serif page title; the rail's current section a
     raised white pill, as the sidebar's (the hover stays a tint, so two
     never look current); each section's title in the serif, over cards
     with a hairline edge, no shadow and no header rule, card titles in
     the serif; field and group labels in sentence case at 500; row
     titles at 500; hints and meta in --muted; the muted bills' numbers
     in mono navy; radios navy when chosen (Checkbox). Buttons and fields
     keep the app's own shape (button shape is to change app-wide in one
     pass), as do the switches, the Coming soon pill's shape, the --gold
     focus ring and the phone rail. */
  .profile-page .settings-id h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em; overflow-wrap: anywhere;
  }
  .profile-page .settings-id .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); }

  /* The rail: 34px items in --ink-2, the current one a raised pill. */
  .profile-page .settings-nav-item {
    height: var(--nav-item); border-radius: var(--radius-sm); font-weight: 500; color: var(--ink-2);
    transition: background .12s ease, color .12s ease;
  }
  .profile-page .settings-nav-item[aria-selected="true"] {
    background: var(--surface); color: var(--ink); font-weight: 600; box-shadow: var(--shadow-nav-active);
  }

  /* Each section's title, then its cards. */
  .profile-page .settings-lead-title {
    font-family: var(--font-display); font-size: var(--text-lg); font-weight: 500; line-height: 1.2; letter-spacing: -0.01em;
  }
  .profile-page .settings-lead .sub { font-size: var(--text-sm); color: var(--ink-2); max-width: 46rem; text-wrap: pretty; }
  .profile-page .settings-card {
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius-lg);
    padding: var(--space-4); margin-bottom: var(--space-4);
  }
  .profile-page .settings-card > :last-child { margin-bottom: 0; }
  .profile-page .panel { border-color: var(--rule); box-shadow: none; }
  .profile-page .panel-head { border-bottom: 0; padding: var(--space-4) var(--space-4) var(--space-2); }
  .profile-page .panel-head .title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; line-height: 1.2; letter-spacing: -0.01em;
  }
  .profile-page .panel-head .sub { color: var(--ink-2); }
  /* The Coming soon pill beside a serif title: Geist, a borderless tint. */
  .profile-page .soon-badge { font-family: var(--font-sans); border-color: transparent; background: var(--paper-250); color: var(--ink-2); }
  /* Forms inside a card sit in its padding, without the page-level
     form's bottom margin; the digest's groups stack full width. */
  .profile-page .panel form { margin-bottom: 0; }
  .profile-page #password-f { padding: 0 var(--space-4) var(--space-4); }
  .profile-page #prefs-f { display: block; }

  /* Fields: labels at 500, hints in --muted. */
  .profile-page .field-label { font-weight: 500; }
  .profile-page :is(.field-hint, .pref-hint) { color: var(--muted); }
  .profile-page #password-f .sub { color: var(--muted); }

  /* Rows: a 500 title over --muted meta. */
  .profile-page .settings-row-title { font-size: var(--text-sm); font-weight: 500; color: var(--ink); }
  .profile-page .saved-meta { font-size: var(--text-xs); color: var(--muted); }
  .profile-page .muted-bill-number { font-family: var(--mono); font-weight: 500; color: var(--link); }

  /* Notifications: group labels in sentence case, radios navy. */
  .profile-page .pref-group > .pref-label {
    font-size: var(--text-2xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
  }
  .profile-page .pref-check .pref-check-sub { color: var(--muted); }
  .profile-page .pref-check input[type="radio"] { accent-color: var(--link); }

  @media (max-width: 640px) {
    .profile-page .settings-id h1 { font-size: var(--text-xl); }
  }

  /* ── Alerts screen ──────────────────────────────────────────────────
     The history (every change, grouped by client) and the saved-search
     inbox. */
  .digest-group { padding: var(--space-1) 0 var(--space-2); }
  .digest-group + .digest-group { border-top: 1px solid var(--rule); }
  /* A client's name over its changes — the design system's group label
     (TriageGroup): 12px, medium, muted, sentence case as written. */
  .digest-group-head {
    padding: var(--space-3) var(--space-3) var(--space-1); font-size: var(--text-2xs); font-weight: 500;
    color: var(--ink-2);
  }
  .digest-row {
    display: flex; align-items: baseline; gap: var(--space-3); padding: var(--space-2) var(--space-4);
    color: inherit; border-radius: 0;
  }
  @media (hover: hover) { .digest-row:hover { background: var(--accent-soft); } }
  /* The same row where it isn't a link of its own (change_row.js) — under
     a dashboard bill, whose row is the link, and in the bill page's "New
     since" strip: no hover wash, no inset of its own. */
  .digest-row.static { padding: var(--space-1) 0; }
  @media (hover: hover) { .digest-row.static:hover { background: none; } }
  /* Date, kind, then the change — the date sits right before the line
     it dates (Sep 29 audit Y16). A hearing's time goes under its day.
     Wide enough for "Sep 19, 2026" on one line: every date carries its
     year, since a two-year session makes a bare "Sep 19" ambiguous. */
  .digest-when { flex: none; width: 5.5rem; font-size: var(--text-xs); font-weight: 600; color: var(--slate); white-space: nowrap; }
  .digest-time { display: block; font-size: var(--text-2xs); font-weight: 400; }
  .digest-type {
    flex: none; width: 7rem; display: inline-flex; align-items: baseline; gap: var(--space-1);
    font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.03em; padding-top: var(--space-0);
  }
  /* Each kind its own colour AND its own glyph, so Hearings and
     Amendments (both amber once) differ without colour too. */
  .digest-type.type-status { color: var(--info); }
  .digest-type.type-amendment { color: var(--warn); }
  .digest-type.type-hearing { color: var(--ink); }
  .digest-type.type-vote { color: var(--good); }
  .digest-type.type-status .digest-icon::before { content: "\25B8"; }          /* moved a step */
  .digest-type.type-amendment .digest-icon::before { content: "\270E"; }       /* pencil: text rewritten */
  .digest-type.type-hearing .digest-icon::before { content: "\2696\FE0E"; }   /* scales */
  .digest-type.type-vote .digest-icon::before { content: "\00B1"; }            /* ayes and noes */
  .digest-type.type-action { color: var(--slate); }
  .digest-type.type-action .digest-icon::before { content: "\2192"; }          /* arrow: a step at the Capitol */
  .digest-redline { font-size: var(--text-xs); font-weight: 600; white-space: nowrap; }
  .digest-body { flex: 1 1 auto; min-width: 0; }
  .digest-bill { font-family: var(--mono); font-size: var(--text-xs); font-weight: 600; color: var(--ink); margin-right: var(--space-2); }
  .digest-summary { font-size: var(--text-sm); color: var(--slate); }
  /* When the refresh saw it, when that differs from when it happens —
     a secondary fact, so it shows on hover or keyboard focus only (and
     to a screen reader always: clipped, not display:none). */
  .digest-noticed {
    margin-left: var(--space-1); font-size: var(--text-2xs); color: var(--slate);
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap;
  }
  .digest-row:focus-visible .digest-noticed { position: static; width: auto; height: auto; overflow: visible; clip: auto; }
  @media (hover: hover) { .digest-row:hover .digest-noticed { position: static; width: auto; height: auto; overflow: visible; clip: auto; } }
  /* Reported two or more days after it happened (Oct 7 audit A8): not a
     secondary fact any more — it explains why old news is new — so it
     shows inline everywhere, a phone included, where hover never comes. */
  .digest-reported { margin-left: var(--space-1); font-size: var(--text-2xs); color: var(--slate); white-space: nowrap; }
  /* A phone can't spare the type and date columns beside the sentence —
     it wrapped a hearing line to seven lines in a 70px strip. There the
     date and type share the top line and the sentence gets the width. */
  @media (max-width: 640px) {
    .digest-row { flex-wrap: wrap; row-gap: var(--space-0); }
    .digest-when { width: auto; }
    .digest-time { display: inline; margin-left: var(--space-1); }
    .digest-type { width: auto; flex: 1 1 auto; }
    .digest-body { order: 1; flex-basis: 100%; }
  }

  /* The digest email's setting — its own quiet box above the tabs. As a
     line inside the history panel it read as one more of the filters
     beneath it (Sep 24 audit); it isn't a view control, it's a setting,
     and it's changed in Settings. */
  .alerts-email {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-3);
    padding: var(--space-2) var(--space-3); margin-bottom: var(--space-4);
    border: 1px solid var(--rule); border-radius: var(--radius-md); background: var(--surface);
    font-size: var(--text-xs); color: var(--ink-2);
  }
  .alerts-email-label { font-weight: 500; color: var(--ink); }
  .alerts-email a { margin-left: auto; display: inline-flex; align-items: center; min-height: 24px; }

  /* History's view filters: client, time window, kind of change. */
  .history-filters {
    display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3) var(--space-4);
    padding: var(--space-3) var(--space-4) 0;
  }
  .history-filter {
    display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; max-width: 100%;
    font-size: var(--text-2xs); font-weight: 600; color: var(--slate);
  }
  .history-filter select { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); max-width: 100%; }
  .history-filters .filter-tabs { margin-bottom: 0; }
  /* Phone only: the filters fold behind one button that names them. */
  .history-filters-toggle { display: none; }
  .history-filters-summary { font-weight: 400; color: var(--slate); margin-left: var(--space-2); }
  @media (max-width: 640px) {
    .history-filters-toggle {
      display: flex; align-items: baseline; width: calc(100% - 2 * var(--space-4));
      margin: var(--space-3) var(--space-4) 0; text-align: left;
    }
    .history-filters-summary { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .history-filters:not(.open) { display: none; }
  }
  .history-more { padding: var(--space-2) var(--space-4) var(--space-4); }

  /* The saved-search inbox: each search, then what it found. */
  /* Both are headings now (group, search) — not the browser's h3 gaps. */
  .digest-group-head, .inbox-search .saved-name { margin: 0; }
  .inbox-search + .inbox-search { border-top: 1px solid var(--rule); }
  .inbox-search .saved-row { border-bottom: 0; padding-bottom: var(--space-1); }
  /* Phone: the name and query get the full width, the buttons go under. */
  @media (max-width: 640px) {
    .inbox-search .saved-row { flex-wrap: wrap; }
    .inbox-search .saved-main { flex-basis: 100%; }
  }
  .inbox-matches { list-style: none; margin: 0; padding: 0 0 var(--space-2); }
  .inbox-match {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-3);
    padding: var(--space-1) var(--space-4);
  }
  .inbox-match-main { flex: 1 1 16rem; min-width: 0; color: inherit; }
  .inbox-match-meta { display: block; font-size: var(--text-2xs); color: var(--slate); }
  .inbox-flagged { font-size: var(--text-xs); color: var(--good); white-space: nowrap; }
  .inbox-note { padding: var(--space-1) var(--space-4) var(--space-3); font-size: var(--text-xs); color: var(--slate); }

  /* Alert-rule On/Off — a switch-shaped button rather than a checkbox,
     so its label ("On"/"Off") reads the same closed and to a screen
     reader (role="switch" + aria-checked carry the state). */
  .rule-actions { display: flex; align-items: center; gap: var(--space-2); flex: none; }
  .switch-btn {
    min-width: 3rem; padding: var(--space-1) var(--space-3); font-size: var(--text-xs); font-weight: 600;
    background: none; border: 1px solid var(--rule-strong); border-radius: var(--radius-pill);
    color: var(--slate); cursor: pointer;
  }
  .switch-btn[aria-checked="true"] { background: var(--good-soft); border-color: var(--good); color: var(--good); }
  @media (hover: hover) { .switch-btn:hover { border-color: var(--ink); } }
  .saved-row.rule-off .saved-name { color: var(--slate); }

  /* ── Alerts v5 (docs/design-system) ──
     Laid over the rules above, scoped to .alerts-page since the history
     filters, change rows, inbox and switch are shared with the calendar,
     the dashboard and a client's page. Serif page and card titles; white
     18px cards with a 4px inset, no shadow and no header rule, so rows
     sit in with their own 12px corners; bill numbers mono navy; the kind
     filter a SegmentedControl; 10px-cornered buttons and fields. */
  .alerts-page .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em;
  }
  .alerts-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }
  .alerts-page .panel {
    background: var(--surface); border: 1px solid var(--rule); box-shadow: none;
    border-radius: var(--radius-lg); padding: var(--space-1); overflow: visible;
  }
  .alerts-page .panel-head { border-bottom: none; padding: var(--space-3) var(--space-3) var(--space-1); align-items: flex-start; }
  .alerts-page .panel-head .title {
    margin: 0; font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; letter-spacing: -0.01em;
  }
  .alerts-page .panel-head .sub { font-size: var(--text-xs); color: var(--ink-2); max-width: 46rem; text-wrap: pretty; }
  .alerts-page .panel-head .sub strong { font-weight: 500; color: var(--ink); }

  /* Buttons: 36px, Geist 500; row actions .small. Corners are the shared button rule's. */
  /* :where() keeps these at one class, so .history-filters-toggle's own
     display rules (hidden on desktop, flex on a phone) still decide. */
  .alerts-page :where(.panel-head > button:not(.linklike), .history-more button, .history-filters-toggle, .modal-actions button) {
    min-height: 2.25rem; padding: 0 var(--space-4);
    font-size: var(--text-sm); font-weight: 500; align-items: center; gap: var(--space-2);
  }
  .alerts-page :where(.panel-head > button:not(.linklike), .history-more button, .modal-actions button) { display: inline-flex; }
  .alerts-page .panel-head > button:not(.linklike) svg { width: 0.85rem; height: 0.85rem; flex: none; }
  .alerts-page .inbox-match button.small { font-weight: 500; }
  /* Header and row text actions read as the dashboard's: muted, no
     underline, ink on hover ("Mark all seen", "Open search →"). */
  .alerts-page .panel-head .linklike, .alerts-page .rule-actions .linklike { font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); text-decoration: none; }
  .alerts-page .rule-actions .linklike { display: inline-flex; align-items: center; min-height: 1.5rem; }
  @media (hover: hover) { .alerts-page .panel-head .linklike:hover, .alerts-page .rule-actions .linklike:hover { color: var(--ink); } }

  /* Filters: the label to the select's left (Select), the kind of change
     a segmented track with its count per option. */
  .alerts-page .history-filters { align-items: center; padding: var(--space-2) var(--space-3) 0; }
  .alerts-page .history-filter { flex-direction: row; align-items: center; gap: var(--space-2); font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); }
  .alerts-page .history-filter select, .alerts-page .modal-panel :is(input, select) { border-radius: var(--radius-sm); }
  .alerts-page .history-filter select { min-height: 1.75rem; padding: 0 var(--space-6) 0 var(--space-2); font-size: var(--text-xs); }
  /* The filters are the same pill dropdowns /flagged uses (.filter-dd*,
     styled with the shared rules in the flagged section); one popover open at a time. */
  .alerts-page .history-filters .filter-dds { gap: var(--space-2); }
  .alerts-page .filter-dd-btn .dd-value { font-weight: 500; }

  /* Change rows: inset, 12px corners, hover to the subtle paper. */
  .alerts-page .digest-group { padding: 0 0 var(--space-1); }
  .alerts-page .digest-group + .digest-group { border-top: none; margin-top: var(--space-1); }
  .alerts-page .digest-row { padding: var(--space-3); border-radius: var(--radius-md); align-items: baseline; }
  /* A row plus its triage ⋮ (alert_triage.js, Oct 7 audit A6): the row
     stays the link, the menu and any "Handed to" mark sit beside it. */
  .triage-row { display: flex; align-items: center; gap: var(--space-2); }
  .triage-row > .digest-row { flex: 1; min-width: 0; }
  .triage-handed { font-size: var(--text-xs); color: var(--ink-2); white-space: nowrap; }
  .triage-handed.mine { color: var(--link); font-weight: 500; }
  /* The row j/k has landed on (triage_keys.js, Oct 7 audit T9). */
  .key-current { outline: 2px solid var(--link); outline-offset: 2px; border-radius: var(--radius-md); }
  .keys-help { display: grid; grid-template-columns: auto 1fr; gap: var(--space-2) var(--space-3); margin: 0; }
  .keys-help dt { margin: 0; text-align: right; }
  .keys-help dd { margin: 0; color: var(--ink-2); }
  .keys-help kbd {
    font-family: inherit; font-size: var(--text-xs); padding: 0 var(--space-1); border: 1px solid var(--rule-card);
    border-radius: var(--radius-sm); background: var(--surface);
  }
  @media (hover: hover) { .alerts-page .digest-row:hover { background: var(--paper-50); } }
  .alerts-page .digest-when { font-size: var(--text-xs); font-weight: 400; color: var(--ink-2); }
  .alerts-page .digest-type { font-size: var(--text-xs); font-weight: 500; text-transform: none; letter-spacing: 0; }
  .alerts-page .digest-bill { font-weight: 400; color: var(--link); margin-right: var(--space-1); }
  .alerts-page .digest-summary { color: var(--ink); }
  /* "New" is the accent count pill (CountPill tone="accent"). */
  .alerts-page .unread-dot { background: var(--info-soft); color: var(--link); font-weight: 500; }

  /* Saved searches: each search a group in the card, its finds as rows. */
  .alerts-page .inbox-search + .inbox-search { border-top: 1px solid var(--rule); margin-top: var(--space-1); padding-top: var(--space-1); }
  .alerts-page .inbox-search .saved-row { padding: var(--space-3) var(--space-3) var(--space-1); }
  .alerts-page .inbox-search .saved-name { font-size: var(--text-base); font-weight: 500; }
  .alerts-page .saved-meta { font-size: var(--text-xs); color: var(--ink-2); }
  .alerts-page .inbox-matches { padding: 0; }
  .alerts-page .inbox-match { padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); align-items: center; }
  @media (hover: hover) { .alerts-page .inbox-match:hover { background: var(--paper-50); } }
  .alerts-page .inbox-match-main { font-weight: 500; }
  .alerts-page .inbox-match-main .digest-summary { font-size: var(--text-sm); }
  .alerts-page .inbox-match-meta { font-size: var(--text-xs); font-weight: 400; color: var(--ink-2); }
  .alerts-page .inbox-flagged { font-weight: 500; }
  .alerts-page .inbox-note { padding: var(--space-1) var(--space-3) var(--space-3); color: var(--ink-2); margin: 0; }
  .alerts-page .switch-btn { font-weight: 500; }
  .alerts-page .history-more { padding: var(--space-2) var(--space-3) var(--space-3); }

  @media (max-width: 640px) {
    .alerts-page .page-head h1 { font-size: var(--text-xl); }
    .alerts-page .history-filters-toggle { width: calc(100% - 2 * var(--space-3)); margin: var(--space-2) var(--space-3) 0; }
    .alerts-page .history-filter { flex-wrap: wrap; }
  }
  .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: var(--space-3); margin-bottom: var(--space-4); }
  .stat-card {
    background: var(--surface); border: 1px solid var(--rule-card); box-shadow: var(--shadow-rest);
    border-radius: var(--radius-lg); padding: var(--space-4); transition: box-shadow .15s ease;
  }
  @media (hover: hover) { .stat-card:hover { box-shadow: var(--shadow-hover); } }
  /* Value-first, no icon (mockup parity): the card leads with the big
     number, then the label under it, then the note. The old icon-first
     treatment is gone — markup no longer emits a .stat-icon. */
  .stat-value { font-size: var(--text-xl); font-weight: 600; letter-spacing: -0.01em; line-height: 1.1; }
  .stat-label { font-size: var(--text-xs); font-weight: 500; color: var(--slate); margin-top: var(--space-1); }
  .stat-foot { font-size: var(--text-2xs); color: var(--slate); margin-top: var(--space-2); }
  /* A card meant to hold a whole list/table, with its own header row —
     same visual family as .card, just with a title/subtitle slot. */
  .panel {
    background: var(--surface); border: 1px solid var(--rule-card); box-shadow: var(--shadow-rest);
    border-radius: var(--radius-lg); overflow: hidden;
  }
  .panel-head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
    padding: var(--space-4) var(--space-4); border-bottom: 1px solid var(--rule);
  }
  /* The heading's long description takes the squeeze, not the action:
     a pill button shrunk beside it broke "+ New rule" onto two lines. */
  .panel-head > button, .panel-head > a.secondary { flex: none; white-space: nowrap; }
  /* An <h2> since the UX audit's A2 — every panel heading in the app
     was a styled div, which left the product with essentially no
     heading outline for anyone navigating by heading. margin:0 and the
     font-size here are what keep an h2 rendering exactly as the div
     did; without them the browser's own h2 defaults reshape every
     panel head in the app. */
  .panel-head .title { font-size: var(--text-base); font-weight: 600; letter-spacing: -0.005em; margin: 0; }
  /* margin, not margin-top: the global .sub rule carries a var(--space-6) bottom
     margin that was leaving dead space under every panel subtitle. */
  .panel-head .sub { font-size: var(--text-xs); color: var(--slate); margin: var(--space-0) 0 0; }
  /* On a phone or small tablet the title and its control cluster can't
     share a row: justify-content:space-between squeezes the heading into
     a two-line sliver on the left while the controls crowd the right
     (worst on Flagged bills, whose cluster is search + sort + group +
     export). Stack them instead — title on its own full-width line,
     controls in their own wrap-row beneath — the same move the ≤700px
     .search-box rule already assumes by giving the filter box a full
     row. The hint drops its desktop right-alignment since it now leads a
     full-width line rather than trailing the controls. */
  @media (max-width: 768px) {
    .panel-head { flex-direction: column; align-items: stretch; }
  }
  .panel-link {
    height: 1.75rem; display: flex; align-items: center; gap: var(--space-1); border-radius: var(--radius-button);
    border: 1px solid var(--rule); background: var(--accent-soft); color: var(--slate);
    font: inherit; font-size: var(--text-2xs); font-weight: 500; padding: 0 var(--space-2); cursor: pointer; flex: none;
  }
  @media (hover: hover) { .panel-link:hover:not(:disabled) { background: var(--content-bg); } }
  .panel-link:disabled { cursor: not-allowed; opacity: 0.6; }
  .panel table { margin: 0; }
  .panel th { padding: var(--space-2) var(--space-4); }
  .panel td { padding: var(--space-3) var(--space-4); }
  @media (hover: hover) { .panel tbody tr:hover { background: var(--content-bg); } }
  /* Whole-row navigation (see row_nav.js): the bill-search and client
     rows are one big click target that opens the detail page. The pointer
     and hover say the row is live; the keyboard focus highlight fires on
     :has(a:focus-visible) so it shows for Tab-into-the-row (the primary
     cell's real <a>) but not for a mouse click, matching the app's other
     :focus-visible affordances. The row's own <a> keeps its outline for
     the precise indicator; this tints the rest of the row with it. */
  tr.link-row { cursor: pointer; }
  tr.link-row:has(a:focus-visible) { background: var(--accent-soft); }
  /* Organization Search's "+ N other filing variants" disclosure — a
     native <details> rather than a JS-driven toggle, and nothing in it
     is actually hidden data, just collapsed by default (see
     app.py's _cluster_client_mentions). */
  @media (hover: hover) { .variants-row:hover { background: none !important; } }
  .variants-row td { padding: 0; }
  .variants-row details { padding: var(--space-2) var(--space-4); }
  .variants-row summary {
    cursor: pointer; font-size: var(--text-xs); color: var(--slate); font-weight: 500;
    list-style: none; display: flex; align-items: center; gap: var(--space-2);
  }
  .variants-row summary::-webkit-details-marker { display: none; }
  .variants-row summary::before { content: '▸'; display: inline-block; transition: transform .15s ease; }
  .variants-row details[open] summary::before { transform: rotate(90deg); }
  .variant-row {
    display: flex; justify-content: space-between; gap: var(--space-4); padding: var(--space-2) 0 var(--space-2) var(--space-5);
    font-size: var(--text-xs); border-top: 1px solid var(--rule); margin-top: var(--space-2);
  }
  /* The lobbying pages' tables (search results, an organization's
     filings). Their names are the way into each record, so they read as
     links — full ink and underlined — rather than the --accent (slate,
     65% ink) every bare link gets, which on these rows was
     indistinguishable from the plain text beside it. */
  .lobbying-table td a:not(.secondary):not(.tag) { color: var(--ink); text-decoration: underline; text-underline-offset: 0.15em; }
  @media (hover: hover) { .lobbying-table td a:not(.secondary):not(.tag):hover { color: var(--gold); } }
  .lobbying-table .not-reported { color: var(--slate); }
  .lobbying-not-found a { color: var(--ink); text-decoration: underline; }
  .lobbying-table td.report { white-space: nowrap; }
  .lobbying-table .form-code { display: block; font-size: var(--text-2xs); color: var(--slate); }
  .lobbying-table td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
  /* An organization page's linked spellings and groups ("Also listed as",
     "Member of"): one line each, the names as tags with a Remove word in
     the error colour (the .chip-remove rule — × only ever means close),
     then the add control. */
  .name-links { display: grid; gap: var(--space-2); margin: 0 var(--space-4) var(--space-3); }
  .name-links-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); }
  .name-links-row .label { font-size: var(--text-xs); color: var(--slate); min-width: 7.5rem; }
  .name-links .chip-remove {
    background: none; border: 0; padding: 0 var(--space-1); font: inherit; font-size: var(--text-2xs);
    font-weight: 600; color: var(--error); cursor: pointer;
  }
  @media (hover: hover) { .name-links .chip-remove:hover { text-decoration: underline; } }
  .name-links form { display: inline-flex; flex-wrap: wrap; gap: var(--space-1); align-items: center; }
  .name-links input[type="text"] { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); width: 16rem; max-width: 100%; }
  /* Lobbying disclosures' one-click client searches (Y22): the firm's
     own clients as .filter-chip buttons under the search box. A list,
     so a screen reader hears how many; wraps rather than scrolls. The
     chips are 1.5rem tall at least (WCAG 2.5.8). */
  .lobbying-client-chips { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); }
  .lobbying-client-chips > .sub { margin: 0; font-size: var(--text-xs); }
  .lobbying-client-chip-list { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; margin: 0; padding: 0; }
  .lobbying-client-chip { min-height: 1.5rem; }
  /* A result's Type label carries its definition as a .has-tip tooltip.
     The table's scroll wrapper clips below its last rows on desktop, so
     there the tip opens upward — the same fix .result-table uses. */
  @media (min-width: 901px) {
    .lobbying-table tbody tr:nth-last-child(-n+2) .has-tip > .tip { top: auto; bottom: calc(100% + 6px); }
  }
  /* Below 900px, one card per row instead of a sideways-scrolling table
     — the same reflow the bill tables use (see "The bill tables, wide
     and narrow"), generic here: each cell's column name comes from its
     own data-label. A cell with no label (the row's name) leads the
     card as its title. */
  @media (max-width: 900px) {
    .lobbying-table, .lobbying-table tbody, .lobbying-table tr, .lobbying-table td { display: block; width: auto; }
    .lobbying-table thead { display: none; }
    .lobbying-table tr { border-bottom: 1px solid var(--rule-strong); padding: var(--space-3) var(--space-4); }
    .lobbying-table tr:last-child { border-bottom: 0; }
    .panel .lobbying-table td { border: 0; padding: var(--space-1) 0; }
    .panel .lobbying-table td:empty { display: none; }
    /* Label beside its value rather than above it — a filing is seven
       short facts, and stacking each label on its own line left about
       one filing per phone screen. */
    .lobbying-table td[data-label] {
      display: grid; grid-template-columns: 6rem minmax(0, 1fr); column-gap: var(--space-2); align-items: baseline;
    }
    .lobbying-table td[data-label] > * { justify-self: start; }
    .lobbying-table td[data-label]::before {
      content: attr(data-label);
      font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.04em;
      text-transform: uppercase; color: var(--slate);
    }
    .lobbying-table td.lead { font-weight: 600; }
    .lobbying-table td.date, .lobbying-table td.num { white-space: normal; }
    .lobbying-table tr.variants-row { padding: 0; }
    .panel .lobbying-table tr.variants-row td { padding: 0; }
  }

  /* ── Lobbying v5 (docs/design-system) ──
     Laid over the rules above, scoped to .lobbying-page (/lobbying,
     /lobbying/detail and its not-found page), since .lobbying-table,
     .tag, .card and .panel are shared with the bill page's "Lobbying on
     this bill" card and the rest of the app. Serif page and card titles;
     white 18px cards on a hairline with no shadow and no rule under the
     head; table heads a tinted rounded band in sentence case, rows that
     tint on hover; tags as borderless neutral chips in sentence case;
     bill numbers mono navy (BillNumber). Buttons and fields keep the
     app's own shape; position badges keep their borders and glyphs
     (P2-31). */
  .lobbying-page .page-head h1,
  .lobbying-page #detail .card > h1.bill-title {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em; text-wrap: balance; overflow-wrap: anywhere;
  }
  .lobbying-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }
  .lobbying-page .lobbying-not-found h1.bill-title { font-family: var(--font-display); font-weight: 500; font-size: var(--text-lg); letter-spacing: -0.01em; }
  .lobbying-page .lobbying-back {
    display: inline-flex; align-items: center; min-height: 1.5rem; margin: 0;
    font-size: var(--text-xs); font-weight: 500; color: var(--ink-2);
  }
  @media (hover: hover) { .lobbying-page .lobbying-back:hover { color: var(--ink); } }
  .lobbying-page .lobbying-client-chips > .sub { color: var(--ink-2); }
  /* The firm's clients as FilterChips: a borderless neutral pill. */
  .lobbying-page .lobbying-client-chip { background: var(--paper-250); color: var(--ink-2); font-weight: 500; }
  @media (hover: hover) { .lobbying-page .lobbying-client-chip:hover { background: var(--paper-350); color: var(--ink); } }

  /* Cards: hairline edge, no shadow, a serif title and no header rule. */
  .lobbying-page .card, .lobbying-page .panel { border-color: var(--rule); box-shadow: none; }
  .lobbying-page #detail .card { padding: var(--space-5); }
  .lobbying-page #detail .card .bill-desc { color: var(--ink-2); }
  .lobbying-page .card-actions { margin-top: var(--space-3); }
  .lobbying-page .panel-head {
    flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-4);
    border-bottom: 0; padding: var(--space-4) var(--space-4) var(--space-3);
  }
  .lobbying-page .panel-head .title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; letter-spacing: -0.01em;
  }
  .lobbying-page .panel-head .sub, .lobbying-page .panel > .sub { font-size: var(--text-xs); color: var(--ink-2); }
  .lobbying-page .panel > h3.title {
    font-family: var(--font-display); font-size: var(--text-base); font-weight: 500; letter-spacing: -0.01em;
  }
  .lobbying-page .empty { color: var(--ink-2); }

  /* Chips: borderless neutral pills, the words as CAL-ACCESS and the
     firm wrote them (no capitals forced on). */
  .lobbying-page .tag { text-transform: none; letter-spacing: 0; background: var(--paper-250); color: var(--ink-2); }
  .lobbying-page .name-links-row .label { font-weight: 500; color: var(--ink-2); }
  /* Bill numbers, linked or not: mono, navy when they open a bill. */
  .lobbying-page .bill-num {
    padding: 0; border: 0; border-radius: 0; background: none;
    font-family: var(--mono); font-size: var(--text-sm); font-weight: 400; color: var(--ink-2);
  }
  .lobbying-page a.bill-num { color: var(--link); }
  /* "AB 5 AB 2" from one "ABs 5, 2": without the pill's fill, a little
     more room keeps two numbers from reading as one. */
  .lobbying-page .bill-num + .bill-num { margin-left: var(--space-1); }
  /* The session after a position's bill number: a muted aside. */
  .lobbying-page .lobbying-table td.lead .sub { margin-left: var(--space-1); font-size: var(--text-xs); font-weight: 400; color: var(--muted); }
  @media (hover: hover) {
    .lobbying-page a.bill-num:hover { background: none; color: var(--ink); text-decoration: underline; text-underline-offset: 0.15em; }
    .lobbying-page .lobbying-table td a:not(.secondary):not(.tag):hover { color: var(--link); }
  }
  .lobbying-page .lobbying-table .form-code, .lobbying-page .lobbying-table .not-reported { color: var(--muted); }
  .lobbying-page .lobbying-table td.date { color: var(--ink-2); }
  .lobbying-page .variants-row summary { color: var(--ink-2); }

  /* Tables: inset from the card edge, the head a tinted rounded band
     in sentence case, hairline row dividers, a row tint on hover. */
  @media (min-width: 901px) {
    .lobbying-page .panel .table-scroll { padding: 0 var(--space-2) var(--space-2); }
    .lobbying-page .lobbying-table { border-collapse: separate; border-spacing: 0; }
    .lobbying-page .lobbying-table th {
      border-top: 0; border-bottom: 0; background: var(--paper-50);
      font-size: var(--text-xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
    }
    .lobbying-page .lobbying-table th:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
    .lobbying-page .lobbying-table th:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
    .lobbying-page .lobbying-table th:first-child, .lobbying-page .lobbying-table td:first-child { padding-left: var(--space-3); }
    .lobbying-page .lobbying-table th:last-child, .lobbying-page .lobbying-table td:last-child { padding-right: var(--space-3); }
    .lobbying-page .lobbying-table td { padding-top: var(--space-3); padding-bottom: var(--space-3); }
    .lobbying-page .lobbying-table tbody tr:last-child td { border-bottom: 0; }
    .lobbying-page .lobbying-table td.lead { font-weight: 500; }
    @media (hover: hover) { .lobbying-page .lobbying-table tbody tr:not(.variants-row):hover td { background: var(--paper-50); } }
  }
  /* The phone card's cell labels, in sentence case like the column heads. */
  @media (max-width: 900px) {
    .lobbying-page .lobbying-table td[data-label]::before { letter-spacing: 0; text-transform: none; font-weight: 500; color: var(--muted); }
    .lobbying-page .lobbying-table td.lead { font-weight: 500; }
  }
  @media (max-width: 640px) {
    .lobbying-page .page-head h1, .lobbying-page #detail .card > h1.bill-title { font-size: var(--text-xl); }
    .lobbying-page #detail .card { padding: var(--space-4); }
  }
  /* A bill row's icon+title+id block — same .bill-title/.bill-id classes
     the /report page's big card already uses, just sized down here via
     the .bill-row scope rather than given new parallel class names. */
  .bill-row { display: flex; align-items: center; gap: var(--space-2); }
  .bill-row .bill-icon {
    height: 1.75rem; width: 1.75rem; border-radius: var(--radius-md); background: var(--accent-soft);
    display: flex; align-items: center; justify-content: center; flex: none;
  }
  .bill-row .bill-icon svg { width: 0.75rem; height: 0.75rem; color: var(--slate); }
  .bill-row .bill-title { font-size: var(--text-sm); font-weight: 500; margin: 0; }
  /* On the Hearing calendar and Sponsors & votes this line is the bill
     number AND its title; only the number (the link) is set in --mono
     (Y23) — the title reads in the body face like every other title. */
  .bill-row .bill-id { font-family: var(--font-sans); font-size: var(--text-2xs); margin: var(--space-0) 0 0; }
  .bill-row .bill-id a { font-family: var(--mono); }
  /* Flagged Bills inverts the pair above: the identifier leads and the
     title sits under it, muted and clamped. "SB1159" is what a lobbyist
     says out loud and scans a list for, and letting the full title set
     the row height meant six bills filled more than a screen — the list
     stopped being scannable exactly where scanning matters. Separate
     class names rather than a restyle of .bill-title/.bill-id, because
     the calendar and the /report card still want the title to lead:
     there the bill is already named and the title is the new
     information. */
  .bill-row .bill-main { flex: 1; min-width: 0; }
  /* --ink, not --accent: --accent aliases --slate in both themes, which
     is also what .bill-name uses below — the "promoted" identifier came
     out the exact same colour as the title it was supposed to lead, with
     only weight telling them apart. */
  .bill-ident { font-family: var(--mono); font-size: var(--text-xs); font-weight: 600; color: var(--ink); margin: 0; }
  .bill-name {
    font-size: var(--text-xs); color: var(--slate); margin: var(--space-0) 0 0;
    /* Two lines then ellipsis; the full title is on the title attribute. */
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }

  /* Flagged Bills — the mockup's clean grid-aligned rows (Tier 2). The
     <table> element and its card reflow below 900px are kept as-is for
     their semantics and a11y; this restyles the desktop chrome toward the
     mockup: generous row padding, a quiet uppercase header over a hairline
     top rule, hairline row separators and no per-column dividers. Scoped
     to .flagged-table so the client record's own .bill-cards table (which
     wants its default table look) is untouched. The bill number is now an
     <a> — ink at rest via .bill-ident, warming to --gold on hover from the
     base a:hover rule — so the whole identifier links to the report. */
  a.bill-ident { text-decoration: none; }
  /* Flagged's number + title as one link (whole-app audit Y5): the title
     keeps its slate, the number its ink, and both warm on hover. */
  a.bill-link { display: block; color: inherit; text-decoration: none; }
  a.bill-link .bill-ident { display: block; }
  /* The "2 new" tag trails the number it belongs to (whole-app audit Y7). */
  a.bill-link .bill-ident .unread-dot { margin: 0 0 0 var(--space-2); }
  /* The full title on keyboard focus too, not only title= on hover (Y6). */
  a.bill-link:focus-visible .bill-name { display: block; }
  @media (hover: hover) { a.bill-link:hover .bill-ident, a.bill-link:hover .bill-name { color: var(--gold); } }
  @media (min-width: 901px) {
    .flagged-table th {
      padding: var(--space-2) var(--space-3); border-top: 1px solid var(--rule-strong);
      border-bottom: 1px solid var(--rule); font-size: var(--text-2xs); font-weight: 600;
      letter-spacing: 0.06em; text-transform: uppercase; color: var(--slate);
    }
    .flagged-table td { padding: var(--space-4) var(--space-3); border-bottom: 1px solid var(--rule); }
    /* The outer cells inset to the panel-head's own gutter, so the bill
       number lines up under the panel title instead of sitting flush
       against the card border. Desktop only — the <900px card reflow
       sets its own td padding. */
    .flagged-table th:first-child, .flagged-table td:first-child { padding-left: var(--space-4); }
    .flagged-table th:last-child, .flagged-table td:last-child { padding-right: var(--space-4); }
    /* The kebab rides in its own trailing column at the far right edge —
       content-sized so it takes no width from the columns that carry the
       row's information. */
    .flagged-table th.col-rowmenu, .flagged-table td.col-rowmenu {
      width: 1%; white-space: nowrap; text-align: right;
    }
  }

  /* The Next action column: when the user has to do something about this
     bill, which Status alone never said — an Engrossed bill with a
     hearing Monday and an Engrossed bill with nothing scheduled used to
     be the same row. */
  td.next-action { white-space: normal; min-width: 9rem; }
  .next-when { font-weight: 600; font-size: var(--text-xs); }
  .next-when.none { font-weight: 400; color: var(--slate); }
  /* The mockup colours the next-action date itself, not only the chip
     under it. Same red-now / amber-soon split the .next-chip carries, so
     the two never disagree; set on the flagged list only, where an
     urgent hearing is the thing the row exists to surface. */
  .next-when.now { color: var(--error); }
  .next-when.soon { color: var(--warn); }
  .next-where { font-size: var(--text-2xs); color: var(--slate); margin-top: var(--space-0); }
  .next-chip { margin-top: var(--space-1); border-color: currentColor; background: var(--warn-soft); color: var(--warn); }
  /* Today and tomorrow read as error rather than warning — at that range
     the thing to notice is that preparation time is gone, not short. The
     border above carries the same distinction without relying on the
     hue, since red/amber is a pair some users can't separate. */
  .next-chip.now { background: var(--error-soft); color: var(--error); }

  /* A filing's deadline on the Disclosures list. Filing dates are the
     hardest deadline these users have and the one place being late has a
     statutory consequence rather than a client-relations one, so overdue
     is styled as an error rather than a warning, and carries its own
     wording ("3 days overdue") instead of leaning on colour to say it. */
  td.col-due { white-space: normal; min-width: 8rem; }
  .due-when { font-weight: 600; font-size: var(--text-xs); }
  .due-none { font-size: var(--text-xs); color: var(--slate); font-style: italic; }
  .due-chip { margin-top: var(--space-1); border-color: currentColor; }
  .due-chip.soon { background: var(--warn-soft); color: var(--warn); }
  .due-chip.over { background: var(--error-soft); color: var(--error); }

  /* The unread marker on a flagged row (see unreadDot() in
     FLAGGED_BODY). A count in a pill rather than a bare dot: the number
     is already known, and "3 changes you haven't seen" is a more useful
     thing to have said than "something happened here". Sized off the
     bill identifier it sits beside so it reads as a marker on that line
     rather than as a badge of its own. --info, the dashboard's "new"
     colour, not --error: news isn't a fault, and a list of red markers
     read as a list of problems. */
  .unread-dot {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 1.05rem; height: 1.05rem; padding: 0 var(--space-1); margin-right: var(--space-1);
    border-radius: var(--radius-pill); background: var(--info); color: var(--surface);
    font-size: var(--text-2xs); font-weight: 700; line-height: 1; vertical-align: middle;
  }

  /* Visually hidden, still read aloud. The clip-rect incantation was
     already inline on half a dozen labels in the templates; this is the
     same thing with a name, for the places (an unread count, a status
     marker) where the visible form is a shape or a colour and the
     sentence behind it has to exist somewhere. */
  .sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }

  /* The first focusable thing in <body> on every app_shell() page (see
     page() in app.py). Without it, keyboard and switch users tab the
     whole fifteen-item sidebar, the account button and the hamburger
     again on every single navigation — and this is a plain multi-page
     app, so that's every click. WCAG 2.4.1, Level A.

     Not .sr-only + :focus: a clip-rect element is 1x1 even while
     focused, so the revealed link has to have its own box. Parked
     off-screen instead of display:none, because a display:none link
     isn't focusable at all and there'd be nothing to reveal. */
  .skip-link {
    position: absolute; top: 0; left: -9999px; z-index: 100;
    background: var(--accent-solid); color: var(--accent-solid-text);
    padding: var(--space-2) var(--space-4); border-bottom-right-radius: var(--radius-md);
    font-size: var(--text-sm); font-weight: 600;
  }
  .skip-link:focus { left: 0; color: var(--accent-solid-text); }

  /* ── Dashboard ──
     The signed-in landing page (2026 redesign, mockup parity). A row of
     tiles and the "Needs you" list sit over the worklist (workflow audit
     A2) — the top of Flagged bills' Next action order, each bill's
     unseen changes on its own row — then a two-column body: clients
     (name + bill count) and the next 14 days as an agenda list, the full
     calendar one click away. The row cells reuse the Flagged table's
     .next-* / .change-* styles, .position-badge and .status-badge
     unchanged. */

  /* The worklist: a card (the highlight card's surface and edge) with a
     header — title, the unseen count, Mark all seen — over the rows. */
  .worklist {
    background: var(--surface); border: 1px solid var(--rule-card); box-shadow: var(--shadow-rest);
    border-radius: var(--radius-lg); padding: var(--space-5); margin-bottom: var(--space-4);
  }
  .worklist-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-3); flex-wrap: wrap; }
  .worklist-title { display: flex; align-items: baseline; gap: var(--space-1) var(--space-3); flex-wrap: wrap; min-width: 0; }
  .worklist-title h2 { font-size: var(--text-base); font-weight: 600; letter-spacing: -0.005em; margin: 0; }
  .worklist-title .dash-col-eyebrow { text-align: left; }
  /* Mark all seen and Open alerts sit side by side, so they're styled as
     one pair — it was underlined beside a plain link (dashboard audit Q5). */
  .worklist-head .linklike {
    display: inline-flex; align-items: center; min-height: 1.5rem;   /* 24px target, as .panel-head .linklike */
    color: var(--slate); text-decoration: none;
  }
  @media (hover: hover) { .worklist-head .linklike:hover { color: var(--ink); } }
  /* The display above beat [hidden], so "Mark all seen" showed with
     nothing new to mark (found checking whole-app audit Y1). */
  .worklist-head .linklike[hidden] { display: none; }
  .worklist-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1) var(--space-4); }
  .worklist-alerts-link { display: inline-flex; align-items: center; min-height: 1.5rem; color: var(--slate); }
  @media (hover: hover) { .worklist-alerts-link:hover { color: var(--ink); } }
  /* The unseen count and each row's New mark, in the info blue — not
     red: a bill moving is routine news, and red is the app's error
     colour (audit F20). Both are words, so the colour is emphasis, not
     the only carrier of the meaning. */
  .worklist-count { font-size: var(--text-xs); font-weight: 700; color: var(--info); }
  .worklist-foot { margin: var(--space-1) 0 var(--space-3); font-size: var(--text-xs); color: var(--slate); }
  .worklist-list { border-top: 1px solid var(--rule); }
  .worklist-links { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0 var(--space-4); }

  /* One row per bill: bill + what's new | Next action | clients and
     positions | status. Fixed tracks, so the columns line up down the
     list the way the Flagged table's do. */
  .worklist-row {
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 10rem) minmax(0, 15rem) minmax(0, 9.5rem);
    gap: var(--space-2) var(--space-4); align-items: start;
    padding: var(--space-3) 0; border-bottom: 1px solid var(--rule); color: var(--ink);
  }
  .worklist-row:last-child { border-bottom: none; }
  @media (hover: hover) { a.worklist-row:hover .worklist-name { color: var(--gold); } }
  /* Two lines of title before the ellipsis, at every width: one line cut
     phone titles to about two words ("AB 1024 Housing…"). */
  .worklist-name {
    font-size: var(--text-sm); font-weight: 500; overflow: hidden;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
  }
  a.worklist-row:focus-visible .worklist-name { display: block; }
  /* What's new: Alerts' own change rows (.digest-row.static), which wrap
     rather than cut off — they're what the row is read for, and should
     need no click. */
  .worklist-new { margin-top: var(--space-1); }
  .worklist-new-more { font-size: var(--text-2xs); color: var(--slate); }
  .worklist-clients { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); min-width: 0; }
  .worklist-clients .position-badge { max-width: 100%; white-space: normal; text-align: left; }
  .worklist-none { font-size: var(--text-xs); color: var(--slate); }
  .worklist-status { justify-self: end; min-width: 0; }
  /* .status-badge carries a bottom margin for its stacked use on
     /report; in this row it sits on the line, so it's zeroed here. */
  .worklist-status .status-badge { margin-bottom: 0; }
  /* Narrower: the bill takes the whole first line and the other three
     share the second. */
  @media (max-width: 1200px) {
    .worklist-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; }
    .worklist-bill { grid-column: 1 / -1; }
  }
  /* Phone: bill; then Next action beside the status; then the clients
     across the full width. */
  @media (max-width: 640px) {
    .worklist { padding: var(--space-4); }
    .worklist-row { grid-template-columns: minmax(0, 1fr) auto; }
    .worklist-clients { grid-column: 1 / -1; grid-row: 3; }
  }

  /* The at-a-glance strip (dashboard audit R1; Oct 19 critique): one
     slim band of linked counts under Needs you, not a row of big cards
     above it, so the queue leads and a phone reaches the first bill
     sooner. One bordered band split by hairlines, each count its number
     and label over a slate foot. Free-floating pills wrapped raggedly
     (two on a line, one alone below); here a wrapped count grows to
     fill its line, and the dividers are each count's own top and left
     edge, which the band's overflow clips off the outer row and column
     — so no line ever doubles the border or hangs past an empty cell. */
  .dash-tiles {
    display: flex; flex-wrap: wrap; overflow: hidden;
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius-lg);
  }
  .dash-tiles[hidden] { display: none; }
  .dash-tile {
    flex: 1 1 11rem; display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--space-2); row-gap: 0;
    min-width: 0; min-height: 44px; border: none; box-shadow: -1px 0 0 var(--rule), 0 -1px 0 var(--rule);
    padding: var(--space-3) var(--space-4); color: inherit;
    font-size: var(--text-sm); transition: background-color .15s ease;
  }
  .dash-tile[hidden] { display: none; }
  @media (hover: hover) { a.dash-tile:hover { background: var(--paper-50); color: inherit; } }
  /* The count is what a glance reads — the display face at heading size,
     its label sitting on the same baseline. */
  .dash-tile-value { font-family: var(--font-display); font-size: var(--text-lg); font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; }
  .dash-tile-label { font-weight: 500; }
  .dash-tile-foot { flex-basis: 100%; font-size: var(--text-xs); color: var(--slate); }
  .dash-tile-foot:empty { display: none; }

  /* Finish setting up (Oct 7 audit T4): the Needs you card's surface,
     rows of number-or-tick | step and its one line | Done / To do. The
     badge is the client setup strip's .setup-num; the state is a word. */
  .finish-setup {
    background: var(--surface); border: 1px solid var(--rule-card); box-shadow: var(--shadow-rest);
    border-radius: var(--radius-lg); padding: var(--space-5); margin-bottom: var(--space-4);
  }
  .finish-setup[hidden] { display: none; }
  .finish-setup-sub { margin: 0 0 var(--space-3); }
  .finish-setup-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
  .finish-step {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-1) var(--space-3);
    align-items: baseline; padding: var(--space-3) 0; border-bottom: 1px solid var(--rule);
  }
  .finish-step:last-child { border-bottom: none; }
  .finish-step.done .setup-num { color: var(--accent-solid-text); background: var(--accent-solid); }
  .finish-step-text { min-width: 0; display: flex; flex-direction: column; gap: var(--space-0); }
  .finish-step-title { font-size: var(--text-sm); font-weight: 600; }
  .finish-step.done .finish-step-title { font-weight: 400; color: var(--slate); }
  .finish-step-detail { font-size: var(--text-xs); color: var(--slate); }
  .finish-step-state { font-size: var(--text-xs); font-weight: 600; color: var(--slate); white-space: nowrap; }
  /* At least 24px tall to click or tap (WCAG 2.5.8) — they were ~20px
     (Oct 19 audit quick win 7). Same recipe as .needs-none .linklike. */
  .finish-step-title, #finish-setup-hide { display: inline-flex; align-items: center; min-height: 1.5rem; }

  /* Needs you (dashboard audit R2): the worklist's card surface, rows of
     kind word | what and where | how soon. The kind is a word in its
     colour, so the colour is emphasis, never the only carrier. */
  .needs {
    background: var(--surface); border: 1px solid var(--rule-card); box-shadow: var(--shadow-rest);
    border-radius: var(--radius-lg); padding: var(--space-5); margin-bottom: var(--space-4);
  }
  .needs[hidden] { display: none; }
  .needs-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
  .needs-list > li { border-bottom: 1px solid var(--rule); }
  .needs-list > li:last-child { border-bottom: none; }
  .needs-row {
    display: grid; grid-template-columns: 7rem minmax(0, 1fr) auto; gap: var(--space-1) var(--space-4);
    align-items: baseline; padding: var(--space-3) 0; color: var(--ink);
  }
  @media (hover: hover) { a.needs-row:hover .needs-title { color: var(--gold); } }
  .needs-kind { font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--slate); }
  .needs-row.kind-conflict .needs-kind, .needs-row.kind-deadline .needs-kind { color: var(--error); }
  .needs-row.kind-calendar .needs-kind, .needs-row.kind-session_calendar .needs-kind,
  .needs-row.kind-filing .needs-kind,
  .needs-row.kind-compliance .needs-kind,
  .needs-row.kind-letter .needs-kind,
  .needs-row.kind-governor .needs-kind, .needs-row.kind-rereview .needs-kind { color: var(--warn); }
  .needs-row.kind-hearing .needs-kind { color: var(--info); }
  .needs-text { min-width: 0; display: flex; flex-direction: column; gap: var(--space-0); }
  .needs-title { font-size: var(--text-sm); font-weight: 600; }
  .needs-detail { font-size: var(--text-xs); color: var(--slate); }
  /* Whose bill, and where the firm stands (Oct 7 audit A4). */
  .needs-stake { font-size: var(--text-xs); color: var(--ink); }
  .needs-stake.conflict { color: var(--error); font-weight: 600; }
  .needs-when { font-size: var(--text-xs); font-weight: 600; color: var(--slate); white-space: nowrap; }
  .needs-when.soon { color: var(--error); }
  .needs-none { padding: var(--space-3) 0; font-size: var(--text-xs); color: var(--slate); }
  /* Nothing pending (audit Y6): one quiet line, no card. The heading and
     count are dropped from view; the line itself still announces the state. */
  .needs.needs-empty { background: none; border: none; box-shadow: none; padding: 0; }
  .needs-empty .dash-col-head { display: none; }
  .needs-empty .needs-list { border-top: none; }
  .needs-empty .needs-none { padding: var(--space-1) 0; }
  /* "Show 4 more" is a line of its own, so a standalone 24px target
     (WCAG 2.5.8), as .panel-head .linklike. */
  .needs-none .linklike { display: inline-flex; align-items: center; min-height: 1.5rem; }
  /* Phone: the kind word over the text, the countdown under it. */
  @media (max-width: 640px) {
    .needs { padding: var(--space-4); }
    .needs-row { grid-template-columns: minmax(0, 1fr); }
  }

  /* The two-column body — clients, calendar; the calendar a touch wider
     to hold its grid. Columns are borderless (a header, a top rule,
     hairline-divided rows), matching the mockup, rather than the boxed
     .panel the old dashboard used. */
  .dash-cols {
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
    gap: var(--space-6); align-items: start;
  }
  .dash-col-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-3); }
  .dash-col-head h2 { font-size: var(--text-base); font-weight: 600; letter-spacing: -0.005em; margin: 0; flex: none; }
  /* Shrinks and wraps rather than the heading: flex:none here ran a long
     eyebrow off a phone's right edge. */
  .dash-col-eyebrow { font-size: var(--text-2xs); font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: var(--slate); min-width: 0; text-align: right; }
  .dash-col-list { border-top: 1px solid var(--rule); }
  .dash-col-more { display: inline-flex; align-items: center; min-height: 1.5rem; margin-top: var(--space-4); font-size: var(--text-xs); color: var(--slate); }
  .dash-col-more[hidden] { display: none; }
  @media (hover: hover) { .dash-col-more:hover { color: var(--ink); } }

  /* A client row: the name on the left, its bill count on the right.
     Most rows are links; hence the a.dash-row hover rule. */
  .dash-row {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    padding: var(--space-3) 0; border-bottom: 1px solid var(--rule); color: var(--ink);
  }
  .dash-row:last-child { border-bottom: none; }
  @media (hover: hover) { a.dash-row:hover { color: var(--gold); } }
  .dash-row-title {
    min-width: 0; font-size: var(--text-sm); font-weight: 500;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .dash-row-number { font-weight: 600; white-space: nowrap; }   /* "SB 902" never breaks in half */
  .dash-count { min-width: 1.6rem; flex: none; background: var(--accent-soft); color: var(--ink); }

  /* ── Dashboard v5 (docs/design-system, "Rotunda Dashboard v5") ──
     Laid over the rules above: the greeting header, white 18px cards
     with a serif title and inset rows, the three triage groups, the
     change feed, date and letter tiles, a 3:1 two-column body that
     wraps. Rules above still carry what v5 doesn't restyle. */
  .dash-head { align-items: flex-end; margin-bottom: var(--space-5); }
  .dash-head-text { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
  .dash-greeting {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em; margin: 0;
  }
  .page-head h1.dash-greeting { font-family: var(--font-display); font-size: var(--text-display); font-weight: 500; }
  #greeting { font-style: italic; color: var(--ink-2); }
  .dash-meta { margin: 0; font-size: var(--text-sm); color: var(--ink-2); text-wrap: pretty; }
  /* The topbar already shows today's date on wider screens, so the
     greeting's copy shows only on a phone, where the topbar names the
     page instead (Oct 19 audit quick win 3). */
  @media (min-width: 641px) { #dash-date { display: none; } }
  @media (max-width: 640px) { #dash-date:not(:empty) + #freshness:not(:empty)::before { content: " · "; } }
  .dash-head .page-head-actions a { min-height: 2.25rem; }
  /* Phone: the two header buttons can't share a line, so each takes the
     full width, the primary on top — not two ragged widths stacked. */
  @media (max-width: 640px) {
    .dash-head .page-head-actions { flex-direction: column; width: 100%; }
    .dash-head .page-head-actions a { justify-content: center; }
    .dash-head .page-head-actions a.primary { order: -1; }
  }
  .dash-head .page-head-actions a.primary { display: inline-flex; align-items: center; gap: var(--space-1); }

  .dash-cols { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: flex-start; }
  .dash-main { flex: 3 1 38rem; min-width: 0; display: flex; flex-direction: column; gap: var(--space-4); }
  .dash-side { flex: 1 1 18rem; min-width: 0; display: flex; flex-direction: column; gap: var(--space-4); }

  /* Card: white, 18px, a 6px inset (space-1 + hairline) so rows sit in
     with their own 12px radius. No header rule. */
  .dash-card, .finish-setup, .needs, .worklist {
    background: var(--surface); border: 1px solid var(--rule); box-shadow: none;
    border-radius: var(--radius-lg); padding: var(--space-1); margin-bottom: 0;
  }
  #grid > .finish-setup { margin-bottom: var(--space-4); }
  .dash-card .dash-col-head, .worklist-head, .changes-head {
    display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-3);
    padding: var(--space-3) var(--space-3) var(--space-1); margin: 0; justify-content: flex-start;
  }
  .dash-card h2, .worklist-title h2 {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500;
    letter-spacing: -0.01em; margin: 0; white-space: nowrap;
  }
  .dash-head-link { margin-left: auto; font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); display: inline-flex; align-items: center; min-height: 1.5rem; }
  .dash-head-link[hidden] { display: none; }
  @media (hover: hover) { .dash-head-link:hover { color: var(--ink); } }
  .dash-count-pill {
    font-size: var(--text-2xs); font-weight: 500; color: var(--ink-2);
    background: var(--paper-250); border-radius: var(--radius-pill); padding: var(--space-0) var(--space-2);
  }
  .dash-count-pill:empty { display: none; }
  .dash-card-sub { margin: 0; padding: 0 var(--space-3) var(--space-1); }
  .dash-card-foot {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-1) var(--space-4);
    margin-top: var(--space-1); padding: var(--space-2) var(--space-3); border-top: 1px solid var(--rule); font-size: var(--text-xs);
  }
  .dash-card-foot .dash-col-more { margin-top: 0; }
  .dash-card-foot #flagged-more { margin-left: auto; color: var(--ink); font-weight: 500; }
  .finish-setup-sub { padding: 0 var(--space-3); }
  .finish-setup-list, .needs-list, .worklist-list, .dash-col-list, .dash-agenda { border-top: none; }
  .finish-step { padding: var(--space-2) var(--space-3); border-bottom: none; border-radius: var(--radius-md); }

  /* Needs you: triage groups (TriageGroup) of ActionRows. */
  .needs-list > li, .needs-list > li:last-child { border-bottom: none; }
  .triage-label { padding: var(--space-3) var(--space-3) var(--space-1); font-size: var(--text-2xs); font-weight: 500; color: var(--ink-2); }
  .triage-label span { color: var(--muted); font-weight: 400; margin-left: var(--space-1); }
  .triage-group.urgent .triage-label { color: var(--error); }
  .triage-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-0); }
  .needs-item { display: flex; align-items: center; gap: var(--space-1); padding-right: var(--space-2); border-radius: var(--radius-md); }
  .needs-item.urgent { background: var(--urgent-row); }
  @media (hover: hover) { .needs-item:hover { background: var(--paper-50); } }
  .needs-list .needs-row {
    flex: 1; min-width: 0; display: grid; grid-template-columns: 2.25rem minmax(0, 1fr) auto;
    align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-1) var(--space-2) var(--space-3); color: var(--ink);
  }
  @media (hover: hover) { a.needs-row:hover .needs-title { color: var(--ink); } }
  .needs-title { font-size: var(--text-sm); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .needs-detail { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .needs-list .needs-kind { font-size: var(--text-xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--ink-2); }
  .needs-item.urgent .needs-kind { color: var(--warn); }
  .needs-item.urgent .kind-conflict .needs-kind { color: var(--error); }
  .needs-when { color: var(--ink-2); font-weight: 500; }
  .needs-item.urgent .needs-when { color: var(--error); }
  .needs-action { flex: none; min-width: 4.75rem; justify-content: center; }
  .needs-list .needs-none { padding: var(--space-4) var(--space-3); text-align: center; }
  /* Phone: the action goes under its row's text, lined up with it, so
     the text keeps the card's width — beside it, it squeezed a two-line
     item into six lines. */
  @media (max-width: 640px) {
    .needs-item { flex-wrap: wrap; padding-bottom: var(--space-2); }
    .needs-list .needs-row { flex-basis: 100%; column-gap: var(--space-4); }
    /* 12px inset + the 36px tile + the 16px gap: the text's left edge. */
    .needs-item > .needs-action { margin-top: calc(-1 * var(--space-1)); margin-left: var(--space-8); }
  }

  /* The small buttons in rows and card headers (design system Button
     sm: 28px, 6px corners); 44px on a touch screen (the coarse-pointer
     block by .row-menu-btn). */
  a.small, button.small { min-height: 1.75rem; padding: 0 var(--space-3); font-size: var(--text-xs); border-radius: var(--radius-button); display: inline-flex; align-items: center; }
  button.small[hidden] { display: none; }
  .icon-btn {
    flex: none; width: 1.875rem; height: 1.875rem; padding: 0; border: none; background: none;
    border-radius: var(--radius-sm); color: var(--muted); cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  }
  .icon-btn svg { width: 0.9rem; height: 0.9rem; }
  @media (hover: hover) { .icon-btn:hover { background: var(--paper-350); color: var(--ink); } }

  /* IconTile / LetterTile / DateTile — decorative; a word always says it. */
  .icon-tile, .letter-tile {
    flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--radius-sm);
    background: var(--paper-250); color: var(--ink-2);
  }
  .icon-tile { width: 2.25rem; height: 2.25rem; }
  .icon-tile svg { width: 1.1rem; height: 1.1rem; }
  .letter-tile { width: 1.75rem; height: 1.75rem; font-size: var(--text-2xs); font-weight: 600; }
  .tone-oppose { background: var(--error-soft); color: var(--error); }
  .tone-support { background: var(--good-soft); color: var(--good); }
  .tone-watch { background: var(--warn-soft); color: var(--warn); }
  .tone-navy { background: var(--info-soft); color: var(--link); }
  .date-tile {
    flex: none; width: 2.75rem; height: 2.875rem; border-radius: var(--radius-sm); background: var(--paper-250);
    display: flex; flex-direction: column; align-items: center; justify-content: center; line-height: 1.15;
  }
  .date-tile span { font-size: var(--text-2xs); color: var(--ink-2); text-transform: uppercase; letter-spacing: 0.04em; }
  .date-tile b { font-size: var(--text-base); font-weight: 600; }

  /* Recent changes: header with SegmentedControl, then ChangeRows. */
  .changes-sub { font-size: var(--text-xs); color: var(--ink-2); white-space: nowrap; }
  .changes-sub:empty { display: none; }
  .changes-spacer { flex: 1; }
  .segmented { display: flex; gap: var(--space-0); padding: var(--space-0); background: var(--paper-350); border-radius: var(--radius-md); }
  .segmented button {
    border: none; background: transparent; border-radius: var(--radius-sm); padding: 0 var(--space-3);
    min-height: 1.75rem; font: inherit; font-size: var(--text-xs); color: var(--ink-2); cursor: pointer;
    display: inline-flex; align-items: center; gap: var(--space-1); white-space: nowrap;
  }
  .segmented button[aria-pressed="true"] { background: var(--surface); color: var(--ink); font-weight: 500; box-shadow: var(--shadow-rest); }
  .segmented-count { color: var(--muted); font-weight: 400; }
  @media (hover: hover) { .segmented button:hover { color: var(--ink); } }
  .change-feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
  .change-item {
    display: grid; grid-template-columns: 1.5rem minmax(0, 1fr) auto; align-items: start; gap: var(--space-1);
    padding: 0 var(--space-1); border-radius: var(--radius-md);
  }
  /* The triage ⋮ (alert_triage.js, Oct 7 audit A6) sits in the third column. */
  .change-item .triage-menu { margin-top: var(--space-2); }
  @media (hover: hover) { .change-item:hover { background: var(--paper-50); } }
  .seen-dot {
    width: 1.5rem; height: 1.5rem; margin-top: var(--space-3); padding: 0; border: none; background: none;
    border-radius: var(--radius-sm); cursor: pointer; display: flex; align-items: center; justify-content: center;
  }
  .seen-dot span { width: 8px; height: 8px; border-radius: 50%; background: var(--link); box-shadow: inset 0 0 0 1.5px var(--link); }
  .seen-dot[disabled] { cursor: default; }
  .change-item.seen .seen-dot span { background: transparent; box-shadow: inset 0 0 0 1.5px var(--muted); }
  @media (hover: hover) { .seen-dot:not([disabled]):hover { background: var(--paper-350); } }
  .change-link {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: var(--space-3);
    padding: var(--space-3) var(--space-1); color: var(--ink);
  }
  @media (hover: hover) { a.change-link:hover { color: var(--ink); } }
  .change-text .digest-row.static { padding: 0; }
  .change-bill { margin-left: var(--space-1); }
  .change-text { display: flex; flex-direction: column; gap: var(--space-0); min-width: 0; }
  .change-bill { font-size: var(--text-xs); color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .change-chips { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-1); max-width: 14rem; min-width: 0; }
  /* A long position + client name stays inside its column (ellipsis, the
     full words in the title) instead of spilling over the change text. */
  .change-chips .position-badge { max-width: 100%; min-width: 0; overflow: hidden; }
  .change-chips .chip-words { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .dash-row-number { font-family: var(--mono); font-weight: 400; font-size: var(--text-xs); color: var(--link); margin-right: var(--space-1); }

  /* The worklist's rows sit inset like every other card's. */
  .worklist-row { padding: var(--space-3); border-bottom: none; border-radius: var(--radius-md); }
  @media (hover: hover) { a.worklist-row:hover { background: var(--paper-50); } a.worklist-row:hover .worklist-name { color: var(--ink); } }
  .worklist-new { font-size: var(--text-xs); color: var(--link); font-weight: 500; }
  /* In the wide column the bill takes its own line, its cells below. */
  .dash-main .worklist-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; }
  /* …but a phone keeps the worklist's own two-column row, or the status
     lands in a zero-width third column over Next action. */
  @media (max-width: 640px) { .dash-main .worklist-row { grid-template-columns: minmax(0, 1fr) auto; } }
  .dash-main .worklist-bill { grid-column: 1 / -1; }
  .worklist-list .empty-state { margin: var(--space-2); }

  /* Right column: agenda rows lead with a DateTile (the written date
     stays for a screen reader), client rows with a LetterTile. */
  .dash-agenda > li, .dash-agenda > li:last-child { border-bottom: none; }
  .dash-agenda .agenda-item { align-items: center; padding: var(--space-2); border-radius: var(--radius-md); }
  .dash-agenda .agenda-date { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  @media (hover: hover) { .dash-agenda a.agenda-item:hover { background: var(--paper-50); color: var(--ink); } }
  .dash-agenda .agenda-none, .dash-agenda .agenda-more { padding: var(--space-2) var(--space-3); }
  .dash-side .dash-row { justify-content: flex-start; padding: var(--space-2); border-bottom: none; border-radius: var(--radius-md); }
  .dash-side .dash-row-title { flex: 1; font-weight: 400; }
  @media (hover: hover) { .dash-side a.dash-row:hover { background: var(--paper-50); color: var(--ink); } }
  .dash-side .dash-count { background: none; color: var(--ink-2); min-width: 0; }
  .dash-side .empty-state { margin: var(--space-2); }

  .changes-head .worklist-alerts-link { font-size: var(--text-xs); }
  .dash-card-foot:not(:has(> :not([hidden]))) { display: none; }
  .dash-footer { margin: var(--space-5) 0 0; font-size: var(--text-2xs); color: var(--muted); }
  /* Below 900px the two columns stack (flex-wrap would too, a little later). */
  @media (max-width: 900px) {
    .dash-cols > * { flex-basis: 100%; }
  }

  @media (max-width: 640px) {
    .dash-greeting, .page-head h1.dash-greeting { font-size: var(--text-xl); }
    .needs-list .needs-row { grid-template-columns: 2.25rem minmax(0, 1fr); }
    .needs-list .needs-when { grid-column: 2; }
    .needs-title, .needs-detail { white-space: normal; }
    .change-link { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
    .change-chips { justify-content: flex-start; max-width: none; }
    .changes-spacer { display: none; }
  }

  /* The dashboard's agenda: the next two weeks, one line per dated item
     (hearing, filing due, legislative deadline), date first. Each kind
     is said in a word beside its own glyph — the colour is a third cue,
     never the only one (Sep 29 audit Y2; WCAG 1.4.1). */
  .agenda { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
  .agenda > li { border-bottom: 1px solid var(--rule); }
  .agenda > li:last-child { border-bottom: none; }
  .agenda-item { display: flex; gap: var(--space-3); align-items: baseline; padding: var(--space-2) 0; color: var(--ink); }
  @media (hover: hover) { a.agenda-item:hover { color: var(--gold); } }
  /* Wide enough for "Wed, Sep 30, 2026" on one line — a date carries
     its year, since a two-year session makes "Sep 30" ambiguous. */
  .agenda-date { flex: none; width: 7.5rem; font-size: var(--text-2xs); font-weight: 600; color: var(--slate); white-space: nowrap; }
  .agenda-text { min-width: 0; display: flex; flex-direction: column; gap: var(--space-0); }
  .agenda-kind {
    display: inline-flex; align-items: center; gap: var(--space-1);
    font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
  }
  .agenda-kind.kind-hearing { color: var(--info); }
  .agenda-kind.kind-filing { color: var(--warn); }
  .agenda-kind.kind-compliance { color: var(--warn); }
  .agenda-kind.kind-letter { color: var(--warn); }
  .agenda-kind.kind-deadline { color: var(--error); }
  .agenda-kind.kind-committee { color: var(--info); }
  .agenda-kind.kind-firm_hearing { color: var(--info); }
  .agenda-kind.kind-hearing .agenda-icon::before { content: "\2696\FE0E"; }   /* scales */
  .agenda-kind.kind-committee .agenda-icon::before { content: "\2637\FE0E"; } /* a list of bills */
  .agenda-kind.kind-firm_hearing .agenda-icon::before { content: "\2696\FE0E"; } /* scales, like a hearing */
  .agenda-kind.kind-filing .agenda-icon::before { content: "\270E"; }
  .agenda-kind.kind-compliance .agenda-icon::before { content: "\270E"; }          /* pencil */
  .agenda-kind.kind-letter .agenda-icon::before { content: "\2709\FE0E"; }   /* envelope */
  .agenda-kind.kind-deadline .agenda-icon::before { content: "\25F7"; }        /* clock face */
  .agenda-label { font-size: var(--text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .agenda-sub { font-size: var(--text-2xs); color: var(--slate); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .agenda-none { padding: var(--space-3) 0; font-size: var(--text-xs); color: var(--slate); }
  /* On a phone, or any screen without hover, the label's title tooltip
     can't be reached and the sub line has none, so "SB318 · Assembly
     Water, Parks and …" would be all there is. There the dashboard's
     rows wrap too, like the Calendar's below; a desktop with a mouse
     keeps one tidy line each. */
  @media (max-width: 640px), (hover: none) {
    .agenda-label, .agenda-sub { white-space: normal; overflow-wrap: anywhere; }
  }
  /* The same rows on the Calendar's day panels and a client's Calendar
     tab (agenda.js, whole-app audit S3): inset to the panel's edge, and
     a deadline's long sentence wraps rather than cutting off — there is
     room here that the dashboard's column doesn't have. */
  .cal-toolbar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-4); margin-bottom: var(--space-4); }
  .cal-toolbar .cal-tabs { margin-bottom: 0; flex: 1 1 auto; }
  .cal-day { margin-bottom: var(--space-4); }
  .cal-agenda, .client-agenda { border-top: 0; }
  .cal-agenda > li, .client-agenda > li { padding: 0 var(--space-4); }
  .cal-agenda .agenda-label, .cal-agenda .agenda-sub,
  .client-agenda .agenda-label, .client-agenda .agenda-sub { white-space: normal; }
  .agenda-more { padding: var(--space-2) 0 0; font-size: var(--text-xs); color: var(--slate); }
  /* Hearing-room cards (Oct 7 audit L6): one card per committee, time and
     room, a line per bill and client — letter and analysis status at a glance. */
  .room-card { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--rule); }
  .room-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
  .room-head .agenda-label { font-weight: 600; white-space: normal; }
  .room-bills { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
  .room-bills > li { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-2) 0;
    border-top: 1px solid var(--rule); font-size: var(--text-sm); }
  .room-bill { color: var(--ink); text-decoration: none; }
  .room-client { font-size: var(--text-xs); color: var(--slate); overflow-wrap: anywhere; }
  .room-letter.is-done { color: var(--good); }
  .room-letter.is-open { color: var(--warn); }
  .cal-week { font-size: var(--text-base); margin: var(--space-4) 0 var(--space-2); }
  .cal-week-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-2); }
  .cal-week-chips .chip { padding: var(--space-1) var(--space-3); border: 1px solid var(--rule-strong);
    border-radius: var(--radius-pill); font-size: var(--text-xs); color: var(--ink); text-decoration: none; }
  /* A hearing the firm added by hand (Oct 7 audit N6): its "Added by your
     firm" tag, and the Calendar's Edit / Delete under the row. */
  .agenda-added { margin-left: var(--space-1); white-space: nowrap; }
  .agenda-actions { display: flex; gap: var(--space-3); padding: 0 0 var(--space-2); font-size: var(--text-2xs); }
  .hearing-form { display: flex; flex-direction: column; gap: var(--space-3); max-width: 36rem; }
  .hearing-form input, .hearing-form select { display: block; width: 100%; margin-top: var(--space-1); font-weight: 400; }
  .hearing-form-row { display: flex; flex-wrap: wrap; gap: var(--space-3); }
  .hearing-form-row > label { flex: 1 1 10rem; }
  .field-optional { font-weight: 400; color: var(--slate); }
  /* A followed committee's agenda (committee_hearings.py): the count is
     the disclosure's summary, the bills a wrapping list under it, the
     firm's own marked with a Flagged tag — a word, not only a colour. */
  .agenda-bills summary { cursor: pointer; font-size: var(--text-2xs); color: var(--slate); min-height: 1.5rem; }
  .agenda-bills ul { list-style: none; margin: var(--space-1) 0 0; padding: 0;
                     display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3); font-size: var(--text-xs); }
  .agenda-bills li { white-space: nowrap; }
  .agenda-bills li.is-mine { white-space: normal; flex-basis: 100%; font-weight: 600; }
  .agenda-bill-clients { font-weight: 400; color: var(--slate); font-size: var(--text-2xs); }
  /* The Calendar's "Committees you follow" picker. */
  .committee-list { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-2) 0; }
  .committee-chip { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs);
                    border: 1px solid var(--rule); border-radius: var(--radius-pill); padding: var(--space-0) var(--space-1) var(--space-0) var(--space-3); }
  .committee-chip button, .committee-picker button {
    background: none; border: 0; padding: var(--space-0) var(--space-2); min-height: 1.5rem;
    font: inherit; font-size: var(--text-xs); font-weight: 600; color: var(--accent); cursor: pointer;
  }
  .committee-chip button[data-follow="0"] { color: var(--error); }
  .committee-note { color: var(--slate); }
  @media (hover: hover) { .committee-chip button:hover, .committee-picker button:hover { text-decoration: underline; } }
  .committee-picker li { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); font-size: var(--text-xs); }
  .committee-picker ul { list-style: none; margin: 0; padding: 0; }
  .committee-picker { max-height: 18rem; overflow-y: auto; border: 1px solid var(--rule); border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); }
  .committee-picker h3 { font-size: var(--text-xs); margin: var(--space-2) 0 var(--space-1); color: var(--slate); text-transform: uppercase; letter-spacing: 0.04em; }

  /* ── Calendar v5 (docs/design-system) ──
     Laid over the rules above, scoped to .calendar-page since the agenda
     rows, room cards, filters and committee chips are shared with the
     dashboard, Alerts and a client's Calendar tab. Serif page and card
     titles; white 18px cards on a hairline with no shadow and no header
     rule; each day's card led by a DateTile, its rows inset with their
     own 12px corners; labels in sentence case; bill numbers mono navy;
     Upcoming / Past and the calendar-app choice as SegmentedControls;
     10px-cornered fields. Buttons keep the app's own shape. */
  .calendar-page .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em;
  }
  .calendar-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }

  /* The four panels that open from the header (work calendar, deadlines,
     add a hearing, committees): cards with a serif title. */
  .calendar-page .card { border: 1px solid var(--rule); box-shadow: none; padding: var(--space-4); }
  .calendar-page .card > h2.section {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500;
    line-height: 1.2; letter-spacing: -0.01em; margin: 0 0 var(--space-2);
  }
  .calendar-page .card .sub { color: var(--ink-2); margin: 0 0 var(--space-3); max-width: 46rem; text-wrap: pretty; }
  .calendar-page .field-label { font-weight: 500; }
  /* nowrap: the global form's wrap made Chrome stretch the Date / Time
     row to twice its height; the card's own padding ends the form. */
  .calendar-page .hearing-form { flex-wrap: nowrap; margin-bottom: 0; }
  .calendar-page .card .sub:empty { margin: 0; }
  .calendar-page .field-optional, .calendar-page .field-hint { color: var(--muted); }
  /* Fields: TextInput / Select / TextArea — 10px corners, 36px tall. */
  .calendar-page :is(.card, .cal-toolbar, .cal-rail) :is(input:not([type="checkbox"]):not([type="radio"]), select, textarea) { border-radius: var(--radius-sm); }
  /* :where() keeps these light enough for the touch floor below to win. */
  .calendar-page .card :where(input:not([type="checkbox"]):not([type="radio"]), select) { min-height: 2.25rem; padding-top: 0; padding-bottom: 0; }
  .calendar-page .card :where(td select) { min-height: 1.875rem; font-size: var(--text-xs); }
  .calendar-page #feed-platforms { width: fit-content; max-width: 100%; flex-wrap: wrap; margin-top: var(--space-1); }
  /* The pasted calendar's check table: tinted sentence-case head, inset. */
  .calendar-page #deadline-preview .table-scroll { border: 1px solid var(--rule); border-radius: var(--radius-md); }
  .calendar-page #deadline-preview th {
    background: var(--paper-50); font-size: var(--text-2xs); font-weight: 500;
    letter-spacing: 0; text-transform: none; color: var(--muted);
  }
  .calendar-page #deadline-preview tbody tr:last-child td { border-bottom: 0; }

  /* Toolbar: each Select's label to its left; Upcoming / Past segmented. */
  .calendar-page .cal-toolbar { align-items: center; gap: var(--space-2) var(--space-4); }
  .calendar-page .history-filter { flex-direction: row; align-items: center; gap: var(--space-2); font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); }
  .calendar-page .history-filter select { min-height: 1.75rem; padding: 0 var(--space-6) 0 var(--space-2); font-size: var(--text-xs); }

  /* This week / Next week / Later: a group label, and the jump links
     above them borderless tinted chips. */
  .calendar-page .cal-week { font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); margin: var(--space-5) var(--space-1) var(--space-2); }
  .calendar-page .cal-week-chips { margin-bottom: 0; }
  .calendar-page .cal-week-chips .chip {
    display: inline-flex; align-items: center; min-height: 1.5rem; padding: 0 var(--space-3);
    border: 0; background: var(--paper-250); color: var(--ink-2); font-weight: 500;
  }
  @media (hover: hover) { .calendar-page .cal-week-chips .chip:hover { background: var(--paper-350); color: var(--ink); } }

  /* A day: a card with a 4px inset, its head the DateTile, the date in
     the serif and Today / In N days; its rows inset, tinting on hover. */
  .calendar-page .cal-day { border: 1px solid var(--rule); box-shadow: none; padding: var(--space-1); }
  .calendar-page .cal-day .panel-head {
    flex-direction: row; flex-wrap: nowrap; align-items: center; justify-content: flex-start;
    gap: var(--space-3); padding: var(--space-2) var(--space-3) var(--space-1); border-bottom: 0;
  }
  .calendar-page .cal-day .panel-head .title {
    flex: 0 1 auto; min-width: 0; font-family: var(--font-display); font-size: var(--text-md); font-weight: 500;
    line-height: 1.2; letter-spacing: -0.01em; text-wrap: pretty;
  }
  .calendar-page .cal-when { font-size: var(--text-xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--ink-2); white-space: nowrap; }
  .calendar-page .cal-agenda > li { padding: 0; border-bottom: 0; }
  .calendar-page .cal-agenda .agenda-item { padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); }
  @media (hover: hover) { .calendar-page .cal-agenda a.agenda-item:hover { background: var(--paper-50); color: var(--ink); } }
  .calendar-page .agenda-kind { font-weight: 500; letter-spacing: 0; text-transform: none; }
  .calendar-page .cal-agenda .agenda-label { font-size: var(--text-sm); font-weight: 500; }
  .calendar-page .cal-agenda .agenda-sub, .calendar-page .agenda-bills summary,
  .calendar-page .room-client, .calendar-page .agenda-bill-clients { color: var(--ink-2); }
  .calendar-page .agenda-actions { padding: 0 var(--space-3) var(--space-2); }
  /* A hearing room: an inset row, a hairline between its bills; each
     bill number in mono navy (BillNumber). */
  .calendar-page .cal-agenda > li.room-card { padding: var(--space-2) var(--space-3); border-bottom: 0; border-radius: var(--radius-md); }
  .calendar-page .room-head .agenda-label { font-weight: 500; }
  .calendar-page .room-bill strong, .calendar-page .agenda-bills li a {
    font-family: var(--mono); font-weight: 400; color: var(--link); white-space: nowrap;
  }
  @media (hover: hover) { .calendar-page .room-bill:hover strong, .calendar-page .agenda-bills li a:hover { color: var(--ink); } }
  .calendar-page .agenda-bills li.is-mine { font-weight: 500; }
  /* "Added by your firm" / "Flagged" are the app's words, not raw
     CAL-ACCESS values, so they read in sentence case here. */
  .calendar-page .agenda-text .tag { text-transform: none; letter-spacing: 0; }

  /* Committees you follow: borderless tinted chips; picker heads in
     sentence case. */
  .calendar-page .committee-chip { border: 0; background: var(--paper-250); }
  .calendar-page :is(.committee-chip, .committee-picker) button { font-weight: 500; }
  .calendar-page .committee-picker h3 { font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--ink-2); }
  .calendar-page #committees-more summary { font-size: var(--text-sm); font-weight: 500; cursor: pointer; }

  /* The notice's inline link kept the button's tall box, spacing its line
     out from the others; on a phone the three header buttons share one width. */
  .calendar-page .result-note button.linklike { display: inline; min-height: 0; padding: 0; line-height: inherit; vertical-align: baseline; }
  @media (max-width: 640px) {
    .calendar-page .page-head-actions > button { flex: 1 1 100%; justify-content: center; }
  }

  /* ── Calendar grid: Month and Week (calendar_body.html) ──
     A rail of filters beside the grid, the way a calendar app lays out.
     One hue per kind of item, the agenda glyphs' own: hearings blue,
     letters amber, deadlines red. Filing dates take the hue between
     blue and red so a swatch never matches the letters'; a committee's
     hearing is a neutral tint and one the firm added is dashed, so it's
     never mistaken for the Legislature's own schedule. Colour is never
     the only cue — every item's words say what it is. */
  .calendar-page [hidden] { display: none !important; }
  .calendar-page .k-hearing { --k: var(--info); }
  .calendar-page .k-letter { --k: var(--warn); }
  .calendar-page .k-deadline { --k: var(--error); }
  .calendar-page .k-filing { --k: color-mix(in oklch, var(--info), var(--error)); }
  .calendar-page :is(.k-committee, .k-firm_hearing) { --k: var(--ink-2); }

  .calendar-page .cal-layout { display: grid; grid-template-columns: 15rem minmax(0, 1fr); gap: var(--space-5); align-items: start; }
  .calendar-page[data-cal-mode="list"] .cal-layout { align-items: start; }
  .calendar-page .cal-rail { display: flex; flex-direction: column; gap: var(--space-5); position: sticky; top: var(--space-4); }
  .calendar-page .cal-icon-btn {
    display: inline-grid; place-items: center; width: 2rem; height: 2rem; min-height: 0; padding: 0;
    border: 0; border-radius: 50%; background: none; color: var(--ink-2); cursor: pointer;
  }
  .calendar-page .cal-icon-btn svg { width: 1.125rem; height: 1.125rem; }
  @media (hover: hover) { .calendar-page .cal-icon-btn:hover { background: var(--paper-250); color: var(--ink); } }

  /* The small month: jump to any day; a dot under a day with items. */
  .calendar-page .cal-mini-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-1); }
  .calendar-page #cal-mini-title { font-size: var(--text-sm); font-weight: 500; }
  .calendar-page .cal-mini { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); text-align: center; font-variant-numeric: tabular-nums; }
  .calendar-page .cal-mini-dow { font-size: var(--text-2xs); color: var(--muted); padding-bottom: var(--space-1); }
  .calendar-page .cal-mini button {
    position: relative; width: 1.875rem; height: 1.875rem; min-height: 0; margin: 1px auto; padding: 0;
    border: 0; border-radius: 50%; background: none; color: var(--ink); font: inherit; font-size: var(--text-2xs); cursor: pointer;
  }
  .calendar-page .cal-mini button.is-out { color: var(--muted); }
  .calendar-page .cal-mini button.in-week { width: 100%; border-radius: 0; background: var(--paper-250); }
  .calendar-page .cal-mini button.is-selected { box-shadow: inset 0 0 0 1px var(--ink); }
  .calendar-page .cal-mini button.is-today { width: 1.875rem; border-radius: 50%; background: var(--accent-solid); color: var(--accent-solid-text); font-weight: 600; }
  .calendar-page .cal-mini button.has-items::after {
    content: ""; position: absolute; left: calc(50% - 1.5px); bottom: 3px; width: 3px; height: 3px;
    border-radius: 50%; background: var(--gold);
  }
  .calendar-page .cal-mini button.is-today.has-items::after { background: var(--accent-solid-text); }
  @media (hover: hover) { .calendar-page .cal-mini button:not(.is-today):hover { background: var(--paper-350); } }

  .calendar-page .cal-filters { display: grid; gap: var(--space-3); }
  .calendar-page .cal-rail .history-filter { flex-direction: column; align-items: stretch; gap: var(--space-1); }
  .calendar-page .cal-rail .history-filter select { width: 100%; min-height: 2.25rem; border-radius: var(--radius-sm); }

  /* Which kinds to show: a coloured swatch that fills when on. */
  .calendar-page .cal-kinds { border: 0; margin: 0; padding: 0; min-width: 0; }
  .calendar-page .cal-kinds legend { font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); padding: 0; margin-bottom: var(--space-1); }
  .calendar-page .cal-kind {
    position: relative; display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-1) var(--space-2); margin: 0 calc(-1 * var(--space-2));
    border-radius: var(--radius-button); font-size: var(--text-sm); cursor: pointer;
  }
  @media (hover: hover) { .calendar-page .cal-kind:hover { background: var(--paper-250); } }
  .calendar-page .cal-kind input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
  .calendar-page .cal-swatch {
    flex: none; display: grid; place-items: center; width: 1rem; height: 1rem;
    border: 2px solid var(--k); border-radius: var(--radius-xs); color: var(--surface);
  }
  .calendar-page .cal-swatch svg { width: 0.75rem; height: 0.75rem; opacity: 0; }
  .calendar-page .k-firm_hearing .cal-swatch { border-style: dashed; }
  .calendar-page .cal-kind input:checked + .cal-swatch { background: var(--k); border-style: solid; }
  .calendar-page .cal-kind input:checked + .cal-swatch svg { opacity: 1; }
  .calendar-page .cal-kind input:focus-visible + .cal-swatch { outline: 2px solid var(--gold); outline-offset: 2px; }
  .calendar-page .cal-kind-n { margin-left: auto; font-size: var(--text-2xs); color: var(--muted); font-variant-numeric: tabular-nums; }

  /* The bar over the grid: Today, back, forward, the period, the views. */
  .calendar-page .cal-main { position: relative; min-width: 0; }
  .calendar-page .cal-bar { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-3); }
  .calendar-page .cal-nav { display: inline-flex; align-items: center; gap: var(--space-1); }
  .calendar-page .cal-today { min-height: 2rem; padding: 0 var(--space-4); }
  .calendar-page .cal-period {
    margin: 0 auto 0 var(--space-2); font-family: var(--font-display); font-size: var(--text-lg); font-weight: 500;
    letter-spacing: -0.01em; white-space: nowrap;
  }
  .calendar-page[data-cal-mode="list"] .cal-views { margin-left: auto; }
  .calendar-page .cal-undated { font-size: var(--text-sm); color: var(--ink-2); margin: 0 0 var(--space-2); }

  .calendar-page .cal-view {
    display: flex; flex-direction: column; height: clamp(34rem, calc(100dvh - 15rem), 60rem);
    border: 1px solid var(--rule-card); border-radius: var(--radius-md); background: var(--surface); overflow: hidden;
  }
  /* Month and Week fit the window: the page fills the scroll area and the
     grid takes whatever height the header leaves, so only the week's hours
     and a long filter rail scroll. An open panel or a short window still
     scrolls the page, since the grid never shrinks below a usable size.
     List is a list, and a phone stacks (below), so both scroll as before. */
  @media (min-width: 901px) {
    .calendar-page:not([data-cal-mode="list"]) { display: flex; flex-direction: column; min-height: 100%; }
    .calendar-page:not([data-cal-mode="list"]) .cal-layout { flex: 1 0 0; min-height: 26rem; grid-template-rows: minmax(0, 1fr); align-items: stretch; }
    .calendar-page:not([data-cal-mode="list"]) .cal-rail { position: static; min-height: 0; overflow-y: auto; scrollbar-width: thin; gap: var(--space-4); }
    /* Tighter so the whole rail fits a laptop screen: each filter's label
       beside its menu, a smaller month, shorter kind rows. */
    .calendar-page:not([data-cal-mode="list"]) .cal-filters { gap: var(--space-2); }
    .calendar-page:not([data-cal-mode="list"]) .cal-rail .history-filter { display: grid; grid-template-columns: 3.25rem minmax(0, 1fr); align-items: center; gap: var(--space-2); }
    .calendar-page:not([data-cal-mode="list"]) .cal-rail .history-filter select { min-height: 2rem; }
    .calendar-page:not([data-cal-mode="list"]) .cal-mini button { height: 1.625rem; }
    .calendar-page:not([data-cal-mode="list"]) .cal-mini button.is-today { width: 1.625rem; }
    .calendar-page:not([data-cal-mode="list"]) .cal-kind { padding-block: var(--space-0); }
    .calendar-page:not([data-cal-mode="list"]) .cal-main { display: flex; flex-direction: column; min-height: 0; }
    .calendar-page:not([data-cal-mode="list"]) .cal-view { flex: 1; height: auto; min-height: 0; }
  }

  /* An item on the grid: a tint of its kind's hue, its start time first. */
  .calendar-page .cal-ev {
    display: flex; align-items: baseline; gap: var(--space-1); width: 100%; min-height: 0; min-width: 0;
    padding: 1px var(--space-2); text-align: left; overflow: hidden; white-space: nowrap; cursor: pointer;
    font: inherit; font-size: var(--text-2xs); line-height: 1.4; color: var(--ink);
    background: color-mix(in srgb, var(--k) 13%, var(--surface));
    border: 1px solid transparent; border-color: color-mix(in srgb, var(--k) 35%, transparent); border-radius: var(--radius-button);
  }
  .calendar-page .cal-ev-time { flex: none; font-weight: 600; font-variant-numeric: tabular-nums; }
  .calendar-page .cal-ev-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .calendar-page .cal-ev.k-deadline { background: var(--k); color: var(--surface); border-color: transparent; font-weight: 500; }
  .calendar-page .cal-ev.k-committee { background: var(--paper-250); border-color: var(--rule-card); }
  .calendar-page .cal-ev.k-firm_hearing { background: var(--surface); border: 1px dashed var(--rule-strong); }
  .calendar-page .cal-ev.is-informational { color: var(--ink-2); background: var(--paper-50); border-color: var(--rule-card); }
  .calendar-page .cal-ev.is-open { box-shadow: 0 0 0 2px var(--gold); }
  @media (hover: hover) {
    .calendar-page .cal-ev:hover { background: color-mix(in srgb, var(--k) 24%, var(--surface)); }
    .calendar-page .cal-ev.k-deadline:hover { background: color-mix(in srgb, var(--k) 85%, var(--ink)); }
    .calendar-page .cal-ev:is(.k-committee, .k-firm_hearing, .is-informational):hover { background: var(--paper-350); }
  }

  /* Month: weekends narrower, since the Legislature doesn't sit then. */
  .calendar-page .cal-month-head, .calendar-page .cal-month {
    display: grid; grid-template-columns: minmax(0, .6fr) repeat(5, minmax(0, 1fr)) minmax(0, .6fr);
  }
  .calendar-page .cal-month-head div {
    padding: var(--space-2) 0 var(--space-1); text-align: center; font-size: var(--text-2xs); font-weight: 500;
    color: var(--muted); border-bottom: 1px solid var(--rule);
  }
  .calendar-page .cal-month { flex: 1; min-height: 0; }
  .calendar-page .cal-cell {
    display: flex; flex-direction: column; gap: var(--space-0); min-width: 0; min-height: 0; overflow: hidden;
    padding: var(--space-1) var(--space-1) var(--space-2);
    border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
  }
  .calendar-page .cal-cell:nth-child(7n) { border-right: 0; }
  .calendar-page .cal-cell.is-weekend { background: var(--paper-50); }
  .calendar-page .cal-daynum {
    flex: none; align-self: center; display: grid; place-items: center; width: 1.625rem; height: 1.625rem; min-height: 0; padding: 0;
    border: 0; border-radius: 50%; background: none; color: var(--ink); cursor: pointer;
    font: inherit; font-size: var(--text-2xs); font-weight: 500; font-variant-numeric: tabular-nums;
  }
  .calendar-page .cal-cell.is-out .cal-daynum { color: var(--muted); }
  .calendar-page .cal-cell.is-today .cal-daynum { background: var(--accent-solid); color: var(--accent-solid-text); font-weight: 600; }
  @media (hover: hover) { .calendar-page .cal-cell:not(.is-today) .cal-daynum:hover { background: var(--paper-350); } }
  .calendar-page .cal-more {
    min-height: 0; padding: 0 var(--space-2); border: 0; background: none; text-align: left; cursor: pointer;
    font: inherit; font-size: var(--text-2xs); font-weight: 500; color: var(--ink-2);
  }
  @media (hover: hover) { .calendar-page .cal-more:hover { color: var(--ink); text-decoration: underline; } }
  .calendar-page .cal-dots { display: flex; justify-content: center; gap: var(--space-0); }
  .calendar-page .cal-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--k); }

  /* Week: the days across, the hours down, all-day items above. */
  .calendar-page .cal-week-head, .calendar-page .cal-allday, .calendar-page .cal-week-grid {
    display: grid; grid-template-columns: 3.5rem minmax(0, .55fr) repeat(5, minmax(0, 1fr)) minmax(0, .55fr);
  }
  .calendar-page :is(.cal-week-head, .cal-allday) { scrollbar-gutter: stable; overflow: hidden; }
  .calendar-page .cal-week-head { border-bottom: 1px solid var(--rule); }
  .calendar-page .cal-week-day { display: flex; flex-direction: column; align-items: center; padding: var(--space-2) 0 var(--space-1); }
  .calendar-page .cal-week-dow { font-size: var(--text-2xs); font-weight: 500; color: var(--muted); }
  .calendar-page .cal-week-num {
    display: grid; place-items: center; width: 2.25rem; height: 2.25rem; min-height: 0; padding: 0; margin-top: var(--space-0);
    border: 0; border-radius: 50%; background: none; color: var(--ink); cursor: pointer;
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; font-variant-numeric: tabular-nums;
  }
  .calendar-page .cal-week-day.is-today .cal-week-dow { color: var(--ink); }
  .calendar-page .cal-week-day.is-today .cal-week-num { background: var(--accent-solid); color: var(--accent-solid-text); }
  .calendar-page .cal-allday { border-bottom: 1px solid var(--rule-card); }
  .calendar-page .cal-allday-label { padding: var(--space-2) var(--space-2) 0 0; text-align: right; font-size: var(--text-2xs); color: var(--muted); }
  .calendar-page .cal-allday-slot { display: flex; flex-direction: column; gap: var(--space-0); min-width: 0; padding: var(--space-1) var(--space-0); border-left: 1px solid var(--rule); }
  .calendar-page .cal-week-body { flex: 1; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; }
  .calendar-page .cal-week-grid { position: relative; }
  .calendar-page .cal-hours div {
    height: var(--cal-hr); padding-right: var(--space-2); text-align: right; transform: translateY(-0.6em);
    font-size: var(--text-2xs); color: var(--muted); font-variant-numeric: tabular-nums;
  }
  .calendar-page .cal-week-col {
    position: relative; border-left: 1px solid var(--rule);
    background-image: linear-gradient(to bottom, var(--rule) 1px, transparent 1px); background-size: 100% var(--cal-hr);
  }
  .calendar-page .cal-week-col.is-weekend { background-color: var(--paper-50); }
  .calendar-page .cal-block {
    position: absolute; left: var(--space-0); right: var(--space-0); width: auto; display: flex; flex-direction: column; align-items: stretch; gap: 0;
    padding: var(--space-1) var(--space-2); white-space: normal; line-height: 1.3;
  }
  .calendar-page .cal-block > * { flex: none; }
  .calendar-page .cal-block .cal-ev-label {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; font-weight: 500;
  }
  .calendar-page .cal-ev-room { color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .calendar-page .cal-ev-bills { display: flex; flex-wrap: wrap; gap: var(--space-0) var(--space-1); margin-top: var(--space-0); }
  .calendar-page .cal-ev-bills span { font-family: var(--mono); font-weight: 500; white-space: nowrap; }
  /* Hearings at overlapping times: one stack, so each still reads. */
  .calendar-page .cal-clash {
    position: absolute; left: 2px; right: 2px; z-index: 1; display: flex; flex-direction: column; gap: var(--space-0); padding: var(--space-1);
    background: var(--surface); border: 1px solid transparent; border-color: var(--gold); border-radius: var(--radius-button); box-shadow: var(--shadow-rest);
  }
  .calendar-page .cal-clash-head { display: flex; justify-content: space-between; gap: var(--space-1); padding: 0 var(--space-0); font-size: var(--text-2xs); font-weight: 600; }
  .calendar-page .cal-clash-head b { color: var(--gold); font-weight: 600; white-space: nowrap; }
  .calendar-page .cal-clash-ev { flex-direction: column; gap: 0; white-space: normal; }
  .calendar-page .cal-now { position: absolute; left: -1px; right: 0; z-index: 2; height: 2px; background: var(--gold); pointer-events: none; }
  .calendar-page .cal-now::before { content: ""; position: absolute; left: -5px; top: -4px; width: 10px; height: 10px; border-radius: 50%; background: var(--gold); }

  /* The detail card: the event's own row from the list, beside it. */
  .calendar-page .cal-detail {
    position: absolute; z-index: 20; width: 24rem; max-width: 100%; padding: var(--space-2) var(--space-1) var(--space-1);
    background: var(--surface); border: 1px solid var(--rule-card); border-radius: var(--radius-md); box-shadow: var(--shadow-pop);
  }
  .calendar-page .cal-detail-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: 0 var(--space-1) 0 var(--space-3); }
  .calendar-page .cal-detail-day { font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); }
  .calendar-page .cal-detail .cal-agenda { margin: 0; }
  @media (prefers-reduced-motion: no-preference) {
    .calendar-page .cal-detail { animation: cal-detail-in .18s cubic-bezier(.16, 1, .3, 1); }
    @keyframes cal-detail-in { from { opacity: 0; transform: translateY(-4px) scale(.98); } }
  }

  /* A phone: the filters as one strip, the month as dots, and the chosen
     day's rows under it; the detail card is a sheet from the bottom. */
  @media (max-width: 900px) {
    .calendar-page .cal-layout { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }
    .calendar-page .cal-rail { position: static; gap: var(--space-3); }
    .calendar-page .cal-mini-wrap, .calendar-page #cal-views [data-mode="week"] { display: none; }
    .calendar-page .cal-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .calendar-page .cal-kinds legend { display: none; }
    .calendar-page #cal-kinds {
      display: flex; gap: var(--space-2); overflow-x: auto; scrollbar-width: none;
      margin: 0 calc(-1 * var(--space-4)); padding: var(--space-0) var(--space-4);
    }
    .calendar-page .cal-kind { flex: none; margin: 0; padding: var(--space-1) var(--space-3) var(--space-1) var(--space-2); border: 1px solid var(--rule-card); border-radius: var(--radius-pill); white-space: nowrap; }
    .calendar-page .cal-kind-n { margin-left: 0; }
    .calendar-page .cal-period { flex-basis: 100%; order: -1; margin: 0; }
    .calendar-page[data-cal-mode="month"] .cal-views { margin-left: auto; }
    .calendar-page .cal-view { height: auto; }
    .calendar-page .cal-month-head, .calendar-page .cal-month { grid-template-columns: repeat(7, minmax(0, 1fr)); }
    .calendar-page .cal-month { grid-auto-rows: 3.25rem; }
    .calendar-page .cal-cell { align-items: center; gap: var(--space-0); padding: var(--space-1) 0; }
    .calendar-page .cal-cell.is-selected:not(.is-today) .cal-daynum { box-shadow: inset 0 0 0 1px var(--ink); }
    .calendar-page .cal-daynum { width: 2rem; height: 2rem; }
    .calendar-page .cal-day-list { margin-top: var(--space-4); }
    .calendar-page .cal-day-list-title { font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; margin: 0 0 var(--space-2); }
    .calendar-page .cal-detail {
      position: fixed; left: 0; right: 0; bottom: 0; top: auto; width: auto; max-height: 70dvh; overflow-y: auto;
      border-radius: var(--radius-lg) var(--radius-lg) 0 0; box-shadow: var(--shadow-up);
      padding-bottom: clamp(var(--space-4), calc(var(--space-4) + env(safe-area-inset-bottom)), var(--space-8));
    }
  }

  /* A touch screen keeps the app's 44px floor on the compact controls. */
  @media (hover: none), (pointer: coarse) {
    .calendar-page :is(.segmented button, .cal-week-chips .chip, .history-filter select, .card select) { min-height: 2.75rem; }
    .calendar-page :is(.cal-today, .cal-icon-btn, .cal-kind) { min-height: 2.75rem; }
    .calendar-page .cal-icon-btn { width: 2.75rem; }
  }
  @media (max-width: 640px) {
    .calendar-page .page-head h1 { font-size: var(--text-xl); }
    .calendar-page .history-filter { flex-wrap: wrap; }
  }

  /* On a narrow screen the two dashboard columns stack. minmax(0, …)
     as on desktop: a bare 1fr won't shrink below its longest no-wrap
     line. */

  /* ── The bill tables, wide and narrow (P2-23) ──
     Two tables carry a client's position on a bill: the flagged list and
     the client record. Both used to hand a phone a five-column table
     inside a sideways scroller, which put the position <select> — the
     one control worth reaching between hearings — past the right edge,
     reachable only by horizontal drag.

     The wrapper is a class rather than the inline style="overflow-x:auto"
     it was, because below the breakpoint there is nothing left to scroll
     and the wrapper has to stop clipping: overflow-x on a block clips
     overflow-y too (per spec), which is what the row menu's own
     open-up hack exists to work around (see row_menu.js). */
  .table-scroll { overflow-x: auto; }

  /* Wide: let the positions column own the slack. At 1440px the browser
     was giving the width to the title and clipping Clients & positions
     at the card edge; these caps say which columns are content-sized and
     leave the rest to the two that need room to breathe. */
  @media (min-width: 901px) {
    .bill-cards th.col-next, .bill-cards th.col-status {
      width: 1%; white-space: nowrap;
    }
    .bill-cards td.col-next, .bill-cards td.col-status { white-space: nowrap; }
    /* Status holds one short badge; its definition tooltip is absolutely
       placed, so the column can be exactly the badge wide (Y6). */
    .flagged-table th.col-status, .flagged-table td.col-status { width: 1%; }
    .bill-cards th.col-clients, .bill-cards th.col-position { min-width: 15rem; }
    /* Author is a name to call, content-sized. A committee author's
       whole name ("Assembly Committee on Judiciary") is clipped rather
       than widening the column; the tooltip has it in full. */
    .bill-cards th.col-author { width: 1%; white-space: nowrap; }
    .bill-cards td.col-author .author-short {
      display: inline-block; max-width: 8rem; vertical-align: bottom;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
  }
  .bill-cards td.col-author .author-none { color: var(--slate); }

  /* Narrow: one card per bill. The table elements are re-declared as
     blocks, which does drop the grid roles — correct here, since a
     stacked card has no columns to be the third of. What each value
     means comes back as a label per cell, generated from CSS so the row
     builder stays one shape for both layouts. */
  @media (max-width: 900px) {
    .table-scroll { overflow: visible; }
    .bill-cards, .bill-cards tbody, .bill-cards tr, .bill-cards td { display: block; width: auto; }
    .bill-cards thead { display: none; }
    /* --rule-strong, not --rule-card: stacked bill cards are all this
       view has, and this border is the only thing between one bill and
       the next. See the token's own comment.

       Flat rows under a rule, not a bordered card inside the panel's own
       card: the nesting cost 32px of a 375px screen and, with every cell
       on its own labelled line, left one bill per screen (8 bills was
       4,200px). A two-column grid gets that back — the row menu takes
       the top-right corner beside the bill instead of a line of its own,
       and status sits beside next action. Everything else spans. */
    .bill-cards tr {
      position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto;
      column-gap: var(--space-3);
      border-bottom: 1px solid var(--rule-strong); padding: var(--space-3) var(--space-4);
    }
    .bill-cards tr:last-child { border-bottom: 0; }
    .panel .bill-cards td { border: 0; padding: var(--space-1) 0; grid-column: 1 / -1; }
    .bill-cards td.col-bill { grid-column: 1; grid-row: 1; }
    .bill-cards td.col-rowmenu { grid-column: 2; grid-row: 1; align-self: start; }
    .bill-cards td.col-next, .bill-cards td.col-title { grid-column: 1; grid-row: 2; }
    .bill-cards td.col-status { grid-column: 2; grid-row: 2; justify-self: end; }
    /* The client's own table has a title AND a next action; the title
       keeps the row beside status and Next action takes a line under it. */
    .bill-cards.client-bills td.col-next { grid-column: 1 / -1; grid-row: auto; }
    /* "Our take" is its own row in the table; on a phone it closes the
       bill's card rather than starting one of its own. */
    .bill-cards.client-bills tr:has(+ tr.take-row) { border-bottom: 0; padding-bottom: 0; }
    .bill-cards.client-bills tr.take-row { display: block; padding-top: 0; }
    /* The whole card opens the report. The bill number was the only link
       — about 46x15, the smallest target on the most-used screen — so its
       hit area is stretched over the row, and the cells holding their
       own controls are lifted above it. The row menu sits highest: its
       dropdown opens down over the status cell of the same card. */
    .bill-cards td.col-bill a::after { content: ""; position: absolute; inset: 0; }
    .bill-cards td.col-status, .bill-cards td.col-clients,
    .bill-cards td.col-position { position: relative; z-index: 1; }
    .bill-cards td.col-rowmenu { position: relative; z-index: 2; }
    /* The identity of the thing needs no label — it is the card's title,
       and "Bill: CA SB1159" reads as a form field rather than a heading. */
    .bill-cards td.col-next::before,
    .bill-cards td.col-status::before,
    .bill-cards td.col-position::before,
    .bill-cards td.col-clients::before {
      display: block; margin-bottom: var(--space-1);
      font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.04em;
      text-transform: uppercase; color: var(--slate);
    }
    .bill-cards td.col-next::before { content: "Next action"; }
    .bill-cards td.col-status::before { content: "Status"; }
    .bill-cards td.col-clients::before { content: "Clients & positions"; }
    /* The client record already knows whose position it is — the page is
       that client — so the same cell gets the shorter true label. */
    .bill-cards td.col-position::before { content: "Position"; }
    /* Author: one short line on the card, its label beside the name
       rather than above it — a surname doesn't need a line to itself. */
    .bill-cards td.col-author::before {
      content: "Author"; margin-right: var(--space-2);
      font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.04em;
      text-transform: uppercase; color: var(--slate);
    }

    /* Phone-first card (full-app audit, strategic 5): a flagged card was
       357px at 375 wide — two bills a screen. Three lines were spent on
       layout, not content: "LAST CHANGE" wrapping onto two lines beside
       its text, the "In 5 days" chip on a line under the room, and the
       client cell's edit pencil wrapping below the client. Now the chip
       sits beside the date, the label stays on one line, and the pencil
       stays beside the client. */
    .bill-cards td.col-next { display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--space-2); }
    .bill-cards td.col-next::before { flex-basis: 100%; }
    .bill-cards td.col-next > .next-where { flex-basis: 100%; order: 1; }
    .bill-cards td.col-next > .next-chip { margin-top: 0; }
    .bill-cards td.col-clients .clients-summary { flex-wrap: nowrap; align-items: flex-start; }
    .bill-cards td.col-clients .clients-summary-list { min-width: 0; }

    /* The whole point of the breakpoint: the position control, full
       width, at a real touch height, with the client's name above it
       rather than truncated beside it. */
    .bill-cards td.col-clients, .bill-cards td.col-position {
      border-top: 1px solid var(--rule) !important;
      margin-top: var(--space-2); padding-top: var(--space-3) !important;
    }
    .bill-cards td.col-clients .add-client-select { width: 100%; min-height: 2.75rem; }
    /* The client record's Position column holds the shared position
       control; on a phone its parts stack, full width. */
    .bill-cards td.col-position .position-control { flex-direction: column; align-items: stretch; }
    .bill-cards td.col-position select.position-select { width: 100%; }
    /* On the client record the title is its own cell rather than part of
       the bill's identity block, so it gets the heading treatment the
       bill number gets on the flagged list. */
    .bill-cards td.col-title { font-weight: 600; }

    /* The filter rail at this width: the group label sits above its own
       tabs rather than sharing a line with them, since a 4.5rem label
       column plus a wrapped row of client names leaves the tabs about
       two words wide. A long client name gets to be a long tab and
       wrap — this is the one control where truncating the name defeats
       the purpose of having it. */
    .search-filters .filter-group { flex-direction: column; align-items: stretch; }
    .search-filters .filter-group-head { padding-right: 0; border-right: 0; }
    .search-filters .filter-group-label { min-width: 0; }
    .search-filters .filter-tab { text-align: left; white-space: normal; }
  }

  .row-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-1); }
  /* Open + delete on one line at the end of a table row. .icon-btn is
     display:flex (block-level), so beside an inline link it dropped
     under it instead of sitting next to it. */
  .cell-actions { display: inline-flex; align-items: center; gap: var(--space-2); }
  .row-actions a { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); }
  /* A "..." overflow menu for row actions that are real but shouldn't
     shout — a bright red button on every row was too loud for
     something you do occasionally, not the primary action of the row. */
  .row-menu { position: relative; text-align: right; }
  /* The design system's RowMenu trigger: a borderless 30px icon button,
     the same everywhere. A boxed ⋮ on every row of a list was the
     loudest thing in it; the icon alone (--muted, 4.8:1 light / 6.8:1
     dark) carries the control, and a fill appears on hover or while
     its menu is open. */
  .row-menu-btn {
    height: 1.875rem; width: 1.875rem; padding: 0; border: none; border-radius: var(--radius-sm); background: none;
    color: var(--muted); display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer; transition: background .12s ease, color .12s ease;
  }
  .row-menu-btn[aria-expanded="true"] { background: var(--paper-350); color: var(--ink); }
  @media (hover: hover) { .row-menu-btn:hover { background: var(--paper-350); color: var(--ink); } }
  /* flex: none for the same reason .icon-btn svg (above) needs it — an
     <svg> with no width/height HTML attributes collapses to 0 width in
     a flex container's default flex-shrink:1, even with this explicit
     CSS width set. Confirmed via getComputedStyle(): this "..." button
     was rendering the icon at 0px wide, an empty-looking button. */
  .row-menu-btn svg { width: 1rem; height: 1rem; flex: none; }
  /* Touch targets under the ~44px guideline (Apple HIG / WCAG 2.5.5) —
     fine with a mouse pointer's precision, but risky to tap accurately
     with a finger, especially .row-menu-btn sitting right next to other
     row content. Scoped to (pointer: coarse) rather than a width
     breakpoint since the risk is about input precision, not viewport
     size — a touch laptop at desktop width has the same problem, and a
     mouse-driven narrow window doesn't.
     Placed here, after .icon-btn/.panel-link/.row-menu-btn's own base
     rules, on purpose — not just for proximity to .row-menu-btn. All
     four selectors here tie with their unconditional base rule on
     specificity (one class each), so the later rule in source order
     wins regardless of whether this media query matches. .icon-btn
     (defined above, ~line 728) and .side-nav-item (~line 620) happen to
     sit before their base rule either way, but .panel-link (~line 797)
     and .row-menu-btn (directly above) don't — this block used to sit
     above both of those, silently losing the cascade and leaving those
     two buttons stuck at their small desktop size under a coarse
     pointer. Same tie-breaking issue .icon-btn.app-topbar-menu-btn's
     own comment (see STYLE, app-topbar section) works around with an
     extra class instead; source-order suffices here since every base
     rule this needs to beat is already above this point. */
  @media (pointer: coarse) {
    .icon-btn, .row-menu-btn { height: 2.75rem; width: 2.75rem; }
    .panel-link { height: 2.75rem; padding: 0 var(--space-4); }
    .side-nav-item { height: 2.75rem; }
    /* The four rules above name five specific classes and miss the two
       controls a touch user actually reaches for all day: the filter
       tabs on /flagged and /lookup (~24px, and shrunk further to
       0.2rem padding inside .filter-group) and the position select in
       a .client-chip (padding: 0.1rem). Both are covered here by their
       element rather than their class, along with the plain form
       button (~35px), .filter-chip and the toast's two buttons.

       min-height, not height: nothing else in this file sets it on a
       button or select, so this reaches every one of them no matter
       where below this point their own rule is defined or how small a
       padding that rule gives them. (Compare this block's own comment
       above about source order — that problem only exists between
       rules setting the *same* property.)

       Not covered on purpose: a.tag. Its clickable form is a bill
       number inside a sentence of CAL-ACCESS free text (see
       billPills() in LOBBYING_DETAIL_BODY), and WCAG 2.5.8 exempts a
       target inline in a block of text — a 44px pill mid-paragraph
       would be a regression, not a fix. */
    button, select { min-height: 2.75rem; }
    /* .small's own 28px min-height outranks the element rule above. */
    a.small, button.small { min-height: 2.75rem; }
    /* Height alone leaves this one about 16px wide: its whole content
       is a single "×". */
    .toast-close { min-width: 2.75rem; }
    /* The shared position control stacks: full-width dropdown, then the
       date with its label above the field, each at a real touch height. */
    .position-control { flex-direction: column; align-items: stretch; }
    .position-control select.position-select { width: 100%; }
    .position-control .pc-since { flex-direction: column; align-items: stretch; gap: var(--space-0); text-align: left; }
    .position-control .pc-since input[type="date"] { width: 100%; min-height: 2.75rem; }
    /* iOS Safari zooms the whole page into any focused field under 16px,
       and leaves it zoomed. At 375px the flagged filter box, the sort
       menu, every position <select>, the report's "in force since" date
       and the letter body were all 11.5-14px — each tap cost a pinch.
       !important because the floor has to beat every denser component
       size (.pc-since input[type="date"] and friends) regardless
       of where or how specifically that rule is written. */
    input:not([type="checkbox"]):not([type="radio"]), select, textarea { font-size: var(--text-base) !important; }
    /* Standalone links and controls this block's element rules miss,
       each measured under 40px at 375px: the topbar search icon (an <a>,
       38x32), "View all bills/clients", the dashboard's hearing rows
       (36px), the history disclosure (19px), the report's date field
       (25px), and the auth/landing "Sign in"/"Create an account" links. */
    .topbar-search { height: 2.75rem; min-width: 2.75rem; justify-content: center; }
    .dash-col-more, .auth-alt a, .lp-signin { display: inline-flex; align-items: center; min-height: 2.75rem; }
    /* Two classes, so it beats .worklist-head .linklike's 24px (a tie
       on specificity, won here by coming later) — the element-level
       button rule above loses to it. */
    .worklist-head .linklike, .worklist-alerts-link, .setup-links a { min-height: 2.75rem; }
    .agenda-item, .position-history-disclosure summary, input[type="date"] { min-height: 2.75rem; }
  }
  .row-menu-dropdown {
    position: absolute; right: 0; top: calc(100% + 0.25rem); background: var(--surface);
    border: 1px solid var(--rule-card); border-radius: var(--radius-md); padding: var(--space-1); min-width: 9rem;
    box-shadow: var(--shadow-pop); z-index: 20; display: none; flex-direction: column; gap: var(--space-1);
  }
  .row-menu-dropdown.show { display: flex; }
  /* Opens upward instead of down — set by toggleRowMenu() when the table's
     own overflow-x:auto wrapper (which clips overflow-y too, per the CSS
     spec, once overflow-x is anything but visible) would otherwise cut
     the menu off. Hits the last row in the table hardest, since there's
     no more table content below it to grow the wrapper's height for. */
  .row-menu-dropdown.open-up { top: auto; bottom: calc(100% + 0.25rem); }
  .row-menu-dropdown button {
    background: none; border: none; color: var(--error); font: inherit; font-weight: 500; font-size: var(--text-xs);
    text-align: left; padding: var(--space-2) var(--space-2); border-radius: var(--radius-sm); cursor: pointer;
  }
  @media (hover: hover) { .row-menu-dropdown button:hover { background: var(--error-soft); } }
  /* Flagged Bills' kebab carries two neutral links (Open bill report,
     View on LegiScan) above the destructive Archive button — they read as
     ordinary menu rows, not more red actions, and stay ink on hover
     rather than warming to --gold like a plain link would. */
  .row-menu-dropdown a {
    color: var(--ink); font: inherit; font-weight: 500; font-size: var(--text-xs);
    text-decoration: none; text-align: left; padding: var(--space-2) var(--space-2);
    border-radius: var(--radius-sm);
  }
  @media (hover: hover) { .row-menu-dropdown a:hover { background: var(--accent-soft); color: var(--ink); } }
  /* Same anchored-dropdown shape as .row-menu-dropdown (position,
     border, shadow, z-index), but plain neutral rows instead of that
     one's red "destructive action" styling — this is a list of name
     suggestions to pick from (see the Add Client form's live
     CAL-ACCESS autofill), not a menu of things to delete. */
  .autofill-dropdown {
    position: absolute; background: var(--surface); border: 1px solid var(--rule-card);
    border-radius: var(--radius-md); padding: var(--space-1); box-shadow: var(--shadow-pop);
    z-index: 20; display: none; flex-direction: column; gap: var(--space-1); max-height: 14rem; overflow-y: auto;
  }
  .autofill-dropdown.show { display: flex; }
  .autofill-dropdown button {
    background: none; border: none; color: var(--ink); font: inherit; font-size: var(--text-sm);
    text-align: left; padding: var(--space-2); border-radius: var(--radius-sm); cursor: pointer;
  }
  @media (hover: hover) { .autofill-dropdown button:hover { background: var(--accent-soft); } }
  /* Firms/coalitions still show up when a typed name matches one (still
     useful to see), but visually recede behind employer matches — an
     employer is what this app means by "client", so it's the one match
     type that should look like the obvious pick. */
  .autofill-dropdown button.non-employer-match { color: var(--slate); }
  th {
    background: var(--content-bg); font-size: var(--text-2xs); font-weight: 600; color: var(--slate);
    text-transform: uppercase; letter-spacing: 0.04em;
  }
  @media (max-width: 900px) {
    /* Used to be `display: none` — the sidebar (and everything only it
       held: section nav, Profile, account menu, sign-out, dark-mode
       toggle) just vanished below 900px with no replacement, leaving
       every app_shell() page with zero navigation on a phone. Now it's
       an off-canvas drawer instead: same markup, positioned fixed and
       slid off-screen by default, brought on-screen by adding .show
       (see app_shell()'s own script for the hamburger button that
       toggles it plus the backdrop/Escape/outside-click handling). */
    .app-sidebar {
      position: fixed; top: 0; left: 0; z-index: 40; transform: translateX(-100%);
      transition: transform .2s ease, visibility 0s linear .2s; box-shadow: var(--shadow-drawer);
      /* Oct 7 audit T8: closed, the drawer is only slid off-screen, so a
         keyboard or screen reader still met every nav link before the
         page itself. Hidden while closed, the page's content comes first
         and the hamburger is the way in. */
      visibility: hidden;
    }
    .app-sidebar.show { transform: translateX(0); visibility: visible; transition: transform .2s ease; }
    .icon-btn.app-topbar-menu-btn { display: flex; }
    .stat-grid { grid-template-columns: 1fr 1fr; }
  }

    /* ── Position history ──
     Append-only record of every stance change (see position_history in
     schema.sql), on the bill report and the client record. An ordered
     list rather than a table: each entry is one sentence with a
     timestamp under it, and four columns of mostly-empty cells was the
     wrong shape for that. */
  ol.position-history { list-style: none; margin: 0; padding: 0; }
  .position-history-row { padding: var(--space-2) 0; border-bottom: 1px solid var(--rule); }
  .position-history-row:last-child { border-bottom: 0; }
  .position-history-what { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
  .position-history-subject { font-weight: 600; font-size: var(--text-sm); }
  .position-arrow { color: var(--slate); }
  .position-history-reconfirmed { font-size: var(--text-xs); color: var(--slate); }
  .position-history-when { font-size: var(--text-2xs); color: var(--slate); margin-top: var(--space-0); }

  /* ── A bill's notes — signed, dated entries (Oct 7 audit T5) ──
     The same list shape as the position history above: one entry per
     row, who and when on a small line over the words. Edit and Delete
     sit on that line, on the writer's own entries only. */
  .bill-note-add { display: flex; justify-content: flex-end; margin: var(--space-2) 0 var(--space-1); }
  ol.bill-notes { list-style: none; margin: 0; padding: 0; }
  .bill-note { padding: var(--space-2) 0; border-bottom: 1px solid var(--rule); }
  .bill-note:last-child { border-bottom: 0; }
  .bill-note-meta { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
                    font-size: var(--text-2xs); color: var(--slate); }
  .bill-note-actions { display: inline-flex; gap: var(--space-2); margin-left: auto; }
  .bill-note-actions .linklike { display: inline-flex; align-items: center; min-height: 1.5rem; font-size: var(--text-xs); }
  .bill-note-body { white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--text-sm); margin-top: var(--space-0); }
  .bill-note-edit-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-top: var(--space-1); }
  /* ── The meeting and contact log (Oct 7 audit P1; meeting_log.js) ──
     Its timeline is the notes list above, one entry a row that opens to
     the whole entry; its dialog is the shared .modal-panel, Kind and
     stance as .filter-tab pills. Layout only — every colour is an
     existing token. */
  .meeting-log-body { padding: var(--space-2) var(--space-4) var(--space-4); }
  .meeting-entry summary { display: flex; align-items: baseline; gap: var(--space-2); cursor: pointer; font-size: var(--text-sm); }
  .meeting-line { min-width: 0; overflow-wrap: anywhere; }
  .meeting-first { color: var(--slate); }
  .meeting-initials { margin-left: auto; flex: none; font-size: var(--text-2xs); font-weight: 600; color: var(--slate); text-decoration: none; }
  .meeting-full { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-2); }
  .meeting-form .filter-tabs { margin-bottom: var(--space-2); }
  .meeting-who { position: relative; }
  .meeting-who .autofill-dropdown { top: 100%; left: 0; right: 0; }
  .meeting-follow { display: flex; flex-direction: column; gap: var(--space-3); }
  .meeting-follow[hidden] { display: none; }
  .needs-list > li.needs-has-action { display: flex; align-items: baseline; gap: var(--space-3); }
  .needs-has-action > .needs-row { flex: 1; min-width: 0; }
  .needs-has-action > .linklike { flex: none; display: inline-flex; align-items: center; min-height: 1.5rem; }
  /* On the bill report the history is a collapsed disclosure at the foot
     of the "Clients & positions" card (audit S3 — it used to be its own
     panel on a Positions tab that repeated the card). Same native
     <details> + rotating ▸ as Organization Search's variants row. The
     lobbying card's "Possibly about an earlier law" (.lobbying-earlier)
     is the same widget. */
  .position-history-disclosure, .lobbying-earlier { border-top: 1px solid var(--rule); padding: var(--space-2) var(--space-4) var(--space-3); }
  .position-history-disclosure summary, .lobbying-earlier summary {
    cursor: pointer; font-size: var(--text-xs); color: var(--slate); font-weight: 600;
    list-style: none; display: flex; align-items: center; gap: var(--space-2);
  }
  .position-history-disclosure summary::-webkit-details-marker, .lobbying-earlier summary::-webkit-details-marker { display: none; }
  .position-history-disclosure summary::before, .lobbying-earlier summary::before { content: '▸'; display: inline-block; transition: transform .15s ease; }
  .position-history-disclosure[open] summary::before, .lobbying-earlier[open] summary::before { transform: rotate(90deg); }
  .position-history-disclosure summary .count, .lobbying-earlier summary .count { font-weight: 400; }
  .position-history-disclosure > .sub, .lobbying-earlier > .sub { margin: var(--space-2) 0 var(--space-1); }
  /* A vetoed bill's Governor's letter (veto_messages.py), the same
     disclosure: kept as written, line breaks and all. */
  .veto-message-text { white-space: pre-line; font-size: var(--text-sm); line-height: 1.55; margin-top: var(--space-2); max-width: 70ch; }


  /* ── Autosave status (autosave.js) ──
     The one "Saving… / Saved / Couldn't save — Retry" indicator every
     editable surface shows beside its edit (whole-app audit Y18/S8).
     Quiet while saving, green with a tick once saved, the error colour
     with a Retry when it didn't land. Empty (idle) it takes no space. */
  .save-status { font-size: var(--text-xs); color: var(--slate); }
  .save-status[data-state="saved"] { color: var(--good); }
  .save-status[data-state="saved"]::before { content: "\2713\00a0"; }
  .save-status[data-state="error"] { color: var(--error); font-weight: 600; }
  .save-status-retry { font-weight: 600; color: inherit; }

  /* ── Toast ──
     One at a time, bottom left, out of the way of the sidebar and of
     anything a page pins to its own bottom right. Carries the undo for a
     position change (see toast.js), so it has to be reachable by
     keyboard and readable long enough to act on. */
  .toast {
    position: fixed; left: 1rem; bottom: 1rem; z-index: 60;
    display: flex; align-items: center; gap: var(--space-3);
    max-width: min(28rem, calc(100vw - 2rem));
    background: var(--ink); color: var(--surface);
    padding: var(--space-3) var(--space-4); border-radius: var(--radius-md);
    box-shadow: var(--shadow-float);
    font-size: var(--text-xs);
  }
  .toast-text { flex: 1; }
  .toast-action {
    background: transparent; color: inherit; border: 1px solid currentColor;
    font-size: var(--text-xs); font-weight: 700; padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-button); cursor: pointer; flex: none;
  }
  .toast-close {
    background: transparent; color: inherit; border: 0; font-size: var(--text-base);
    line-height: 1; padding: 0 var(--space-0); cursor: pointer; flex: none;
    min-width: 1.5rem; min-height: 1.5rem;
  }
  @media (prefers-reduced-motion: no-preference) {
    .toast { animation: toast-in 140ms ease-out; }
  }
  @keyframes toast-in { from { transform: translateY(0.5rem); opacity: 0; } to { transform: none; opacity: 1; } }

  /* Floating label for icon-only controls (tooltip.js), shown on hover
     and on keyboard focus. position:fixed off <body> so a table cell's
     overflow wrapper can't clip it (same reason .toast lives there);
     z-index clears the modal backdrop (100) so the Close × inside a
     dialog gets one too, and stays under the page-progress bar (200).
     Same dark-on-light chip as .toast — one carries confirmation, the
     other a name — so they read as one family. pointer-events:none so
     it never sits between the cursor and the button it describes. */
  .app-tooltip {
    position: fixed; top: 0; left: 0; z-index: 150; pointer-events: none;
    max-width: 16rem; white-space: nowrap;
    background: var(--ink); color: var(--surface);
    padding: var(--space-1) var(--space-2); border-radius: var(--radius-sm);
    font-size: var(--text-2xs); font-weight: 600; line-height: 1.3;
    box-shadow: var(--shadow-float);
    opacity: 0; transform: translateY(0.15rem);
  }
  .app-tooltip.show { opacity: 1; transform: none; }
  .app-tooltip.below { transform: translateY(-0.15rem); }
  .app-tooltip.below.show { transform: none; }
  @media (prefers-reduced-motion: no-preference) {
    .app-tooltip { transition: opacity .1s ease, transform .1s ease; }
  }

  /* ── Search results ──
     The results table stopped being a read-only list: it now carries a
     per-row flag control, a selection checkbox, and what the user has
     already decided about each bill. */

  /* Right-aligned action cell, still used by the Capitol directory's
     table (directory_body.html). */
  td.flag-cell { white-space: nowrap; text-align: right; }

  /* A bill the user already tracks: dimmed rather than hidden, since the
     useful signal is "you settled this one", not "this isn't a result".
     Applies to the search result cards (.result-card.tracked-row below)
     and any table row that opts in. */
  tr.tracked-row { background: var(--content-bg); }
  .result-tracked { margin-top: var(--space-1); display: flex; flex-wrap: wrap; gap: var(--space-1); font-size: var(--text-2xs); color: var(--slate); }

  /* ── Search results as cards (Tier 1 rebuild) ──
     The dense results table became a list of bordered cards, one per
     result: a bill number + title, the inferred-status pill and — when
     flagged — the client RAG position badges up in the head, a meta line
     of session/chamber/last-action, and two pill actions (Bill report,
     Flag this bill). Same fields the table row carried (see renderRowsNow
     in LOOKUP_BODY), given room to read. The whole card stays a click
     target to the report (class link-row + data-href, row_nav.js); the
     number, title and Bill-report pill are the reachable anchors in it. */
  /* One grid row per card — [select] bill + meta, status + flag — so a
     card is its title's height plus one meta line, not five stacked
     rows (search/dashboard audit S4: 167px each, four to a screen). */
  /* An <ol role="list"> so a screen reader says "list, 25 items" (audit
     L3); the reset keeps it looking exactly as the <div> did. */
  .result-cards { display: flex; flex-direction: column; gap: var(--space-2); list-style: none; margin: 0; padding: 0; }
  /* The results panel has no padding of its own (its head and the
     legend pad themselves, the compact table runs edge to edge), so the
     group heading, the cards and "Show more" sat flush against its
     border at every width. They take the head's inset. */
  .results-panel > .result-group-head, .results-panel > .result-cards,
  .results-panel > .result-more, .results-panel > .save-search-form { margin-inline: var(--space-4); }
  /* The header's View menu drops below the header; .panel's
     overflow: hidden cut it off over a one- or two-result list. */
  .panel.results-panel { overflow: visible; }
  .result-card {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--space-3); align-items: start;
    background: var(--surface); border: 1px solid var(--rule-card);
    border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4);
    cursor: pointer; transition: border-color .12s ease, box-shadow .12s ease;
  }
  .result-card.has-pick { grid-template-columns: auto minmax(0, 1fr) auto; }
  .result-card .pick-label { margin: 0; padding: var(--space-1); }
  .result-card .pick { width: 1rem; height: 1rem; margin: 0; accent-color: var(--accent-solid); }
  .pick-spacer { display: block; width: 24px; }
  @media (hover: hover) { .result-card:hover { border-color: var(--rule-strong); box-shadow: var(--shadow-rest); } }
  /* A bill already tracked reads as settled, not as "not a result" — the
     same dim the table row used (tr.tracked-row above), kept here. */
  .result-card.tracked-row { background: var(--content-bg); }
  .result-card-headline { min-width: 0; padding-top: var(--space-0); }
  .result-card-link { display: block; color: var(--ink); text-decoration: none; }
  @media (hover: hover) {
    .result-card-link:hover .result-card-number,
    .result-card-link:hover .result-card-title { color: var(--accent); }
  }
  .result-more { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-4) 0 0; }
  .result-more .sub { margin: 0; }
  /* The title is the link's headline; the number is its label, leading
     the same line. */
  .result-card-number {
    font-family: ui-monospace, monospace; font-size: var(--text-xs); font-weight: 700;
    letter-spacing: 0.01em; color: var(--slate); margin-right: var(--space-2);
  }
  @media (hover: hover) { .result-card-number:hover { color: var(--accent); } }
  .result-card-title {
    font-size: var(--text-base); font-weight: 600; line-height: 1.4; color: var(--ink);
  }
  @media (hover: hover) { .result-card-title:hover { color: var(--accent); } }
  /* The head's right rail: the inferred-status pill, and beneath it the
     RAG position badges when the bill is flagged, right-aligned so the
     two kinds of status stack clear of the title. */
  .result-card-flags {
    flex: none; display: flex; flex-direction: column; align-items: flex-end;
    gap: var(--space-1); text-align: right;
  }
  .result-card-flags .status-badge { margin-bottom: 0; }
  .result-card-flags .result-tracked { margin-top: 0; justify-content: flex-end; }
  .result-card-flags .flag-toggle { margin-left: 0; }
  .result-card .match-note { margin-top: var(--space-1); }
  /* Session as a label + value pair, then the last action — its date and
     what it was. */
  .result-card-meta {
    display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); margin-top: var(--space-1);
    font-size: var(--text-xs); color: var(--slate);
  }
  .result-card-meta strong { font-weight: 600; color: var(--ink); }
  /* One line however long the action ("Enrolled and presented to the
     Governor at 3 p.m."), ellipsized, the whole text in its title. */
  .result-card-meta .result-card-action {
    min-width: 0; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .result-card-meta .match-meta { font-style: italic; }
  /* The label is the hit area — 24px tall at least, word included. */
  .pick-label {
    display: inline-flex; align-items: center; gap: var(--space-2); min-height: 24px; min-width: 24px;
    padding: var(--space-1) var(--space-2) var(--space-1) var(--space-1); margin-left: calc(-1 * var(--space-1)); cursor: pointer;
    font-size: var(--text-xs); color: var(--slate); border-radius: var(--radius-sm);
  }
  @media (hover: hover) { .pick-label:hover { background: var(--content-bg); } }
  /* Flag as an icon toggle: outline at rest, filled once pressed. */
  .flag-toggle {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
    min-width: 32px; min-height: 32px; padding: 0 var(--space-2); margin-left: auto;
    background: transparent; color: var(--slate); border: 1px solid var(--rule-strong);
    border-radius: var(--radius-button); font: inherit; font-size: var(--text-xs); font-weight: 600; cursor: pointer;
  }
  .flag-toggle svg { width: 14px; height: 14px; }
  @media (hover: hover) { .flag-toggle:hover { color: var(--accent); border-color: var(--accent); } }
  .flag-toggle[aria-pressed="true"] { color: var(--accent); background: var(--accent-soft); border-color: transparent; cursor: default; }
  .flag-toggle[aria-pressed="true"] svg path { fill: currentColor; }
  /* Flagged, but more clients it could be flagged for: still a button. */
  .flag-toggle-more[aria-pressed="true"] { cursor: pointer; }
  @media (hover: hover) { .flag-toggle-more[aria-pressed="true"]:hover { border-color: var(--accent); } }
  .flag-more { font-weight: 700; }
  /* Cards stack full-width on a phone, and the head's right rail drops
     under the title rather than fighting it for the narrow row. */
  @media (max-width: 640px) {
    /* The title takes the full width; the select box drops into the
       status/flag line beneath it rather than narrowing every title by
       its column. */
    .result-card, .result-card.has-pick { grid-template-columns: auto minmax(0, 1fr); }
    .result-card-headline { grid-column: 1 / -1; grid-row: 1; }
    .result-card > .pick-label { grid-column: 1; grid-row: 2; align-self: center; margin-top: var(--space-2); }
    .result-card > .pick-spacer { display: none; }
    .result-card-flags {
      grid-column: 2; grid-row: 2; flex-direction: row; flex-wrap: wrap; align-items: center;
      text-align: left; margin-top: var(--space-2);
    }
    /* Status and flag share a line; the client chips, when there are
       any, take the next one. */
    .result-card-flags .result-tracked { justify-content: flex-start; order: 3; flex-basis: 100%; }
    .result-card-flags .flag-toggle { margin-left: auto; }
  }

  /* The bulk-flag bar, present only while something is selected — an
     empty "0 selected" toolbar would be permanent furniture for an
     occasional action. It follows the results and pins to the bottom of
     the window while they scroll, so it is always beside the box just
     ticked. The tint is layered over the page surface because the bar
     now sits over cards as they scroll past; the soft tint alone lets
     them show through. */
  .selection-bar {
    position: sticky; bottom: 0; z-index: 5;
    display: flex; align-items: center; gap: var(--space-3); flex-wrap: nowrap;
    background: linear-gradient(var(--accent-soft), var(--accent-soft)), var(--surface);
    color: var(--accent); box-shadow: var(--shadow-hover);
    border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-top: var(--space-3);
  }
  /* One line at desktop widths: "Clear" used to wrap to a second line at
     1280px, doubling the bar over the cards. Nothing in it wraps its
     words; a long client name narrows its own picker (ellipsized) rather
     than pushing the buttons down. Only a phone wraps the bar. */
  .selection-bar > * { flex: none; white-space: nowrap; }
  .selection-bar > label { display: inline-flex; align-items: center; gap: var(--space-1); }
  .selection-bar > label:first-of-type { flex: 0 1 auto; min-width: 0; }
  .selection-count { font-weight: 700; font-size: var(--text-sm); }
  .selection-bar select { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); }
  .selection-bar > label:first-of-type select { min-width: 0; max-width: 100%; text-overflow: ellipsis; }
  /* A phone wraps it to three rows rather than five: the count with
     Clear, the client picker the full width, then position and Flag. */
  @media (max-width: 640px) {
    .selection-bar { flex-wrap: wrap; gap: var(--space-2) var(--space-3); }
    .selection-bar > .secondary { order: 1; margin-left: auto; }
    .selection-bar > label:first-of-type { order: 2; flex: 1 1 100%; }
    .selection-bar > label:first-of-type select { flex: 1 1 auto; }
    .selection-bar > label ~ label { order: 3; }
    .selection-bar > #bulk-flag-btn { order: 4; }
  }
  /* The bar keeps its own space after the last card, being in the flow;
     this keeps a card tabbed to (or scrolled into view) from landing
     behind it while it is pinned over the list. */
  html:has(.selection-bar) { scroll-padding-bottom: calc(var(--space-8) + var(--space-5)); }
  /* A row's "Flag AB 610 for [client] as [position]", opened by its flag
     button (see flagDraftHtml in LOOKUP_BODY) — the selection bar's two
     choices, for one bill, in the row itself: a full-width last row of
     the card's grid, or a row of its own under a table row. In the flow
     rather than floating, so a table's scroll wrapper can't clip it. */
  .flag-draft {
    grid-column: 1 / -1; display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-3);
    margin: var(--space-3) 0 0; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
    background: var(--accent-soft); color: var(--accent); cursor: default;
  }
  .flag-draft-label { font-weight: 700; font-size: var(--text-sm); }
  .flag-draft select { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); }
  /* A long client name sized the select past a phone-width card. The
     label is a shrinkable flex row, so the select can give way inside it
     (max-width alone resolves against a label sized by the select). */
  .flag-draft label { display: inline-flex; align-items: center; gap: var(--space-1); min-width: 0; max-width: 100%; }
  .flag-draft select { min-width: 0; max-width: 100%; text-overflow: ellipsis; }
  .flag-draft-row td { padding-top: 0; }
  .flag-draft-row .flag-draft { margin-top: 0; }

  /* ── Search results as a compact table ──
     The same rows as the cards, one line each, for triaging a long
     result set (see resultTableHtml in LOOKUP_BODY). Content-sized
     columns hold to their content; the title takes the slack and wraps
     to at most two lines, and cells pad only --space-1 top and bottom, so
     a row is ~48px and a 1280x800 screen holds 12+ bills (checklist L4;
     untrimmed, a long title wrapped to six lines in a ~200px column).
     The last action's text is one ellipsized line under its date, whole
     in the title tooltip. Scrolls sideways when the column is too narrow
     for it (a laptop with both sidebars open, or a phone); the last two
     rows' status tips open upward so they don't stretch that scroll box
     from the bottom. */
  .result-table-wrap { overflow-x: auto; }
  .result-table { width: 100%; min-width: 42rem; border-collapse: collapse; font-size: var(--text-sm); }
  .result-table th {
    text-align: left; font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.04em;
    text-transform: uppercase; color: var(--slate); padding: 0 var(--space-2) var(--space-2); border-bottom: 1px solid var(--rule-strong);
  }
  .result-table td { padding: var(--space-1) var(--space-2); border-bottom: 1px solid var(--rule); vertical-align: top; }
  .result-table tbody tr { cursor: pointer; }
  @media (hover: hover) { .result-table tbody tr:hover { background: var(--content-bg); } }
  .result-table th.col-pick, .result-table td.col-pick { width: 1%; padding-right: 0; }
  .result-table .pick-label { margin-left: 0; padding: var(--space-1); }
  .result-table .pick { width: 1rem; height: 1rem; margin: 0; accent-color: var(--accent-solid); }
  .result-table .col-bill, .result-table .col-status, .result-table .col-chamber, .result-table .col-flag { width: 1%; white-space: nowrap; }
  .result-table td.col-bill a { font-family: ui-monospace, monospace; font-size: var(--text-xs); font-weight: 700; color: var(--ink); }
  @media (hover: hover) { .result-table td.col-bill a:hover { color: var(--accent); } }
  .result-table td.col-title { line-height: 1.4; }
  .result-table .col-title-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
  .result-table td.col-title .result-tracked { margin-top: var(--space-1); }
  .result-table .status-badge { margin: 0; }
  .result-table .col-action { width: 11rem; max-width: 11rem; }
  .result-table .col-action-date { white-space: nowrap; font-variant-numeric: tabular-nums; }
  .result-table .col-sub {
    display: block; margin-top: var(--space-0); font-size: var(--text-2xs); color: var(--slate);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .result-table td.col-action .col-sub { max-width: 11rem; }
  .result-table tbody tr:nth-last-child(-n+2) .has-tip > .tip { top: auto; bottom: calc(100% + 6px); }
  .result-table td.col-flag .flag-toggle { margin-left: 0; }
  /* The two drawings, first in the results header's View menu. The
     current one carries a check; a dropdown button is otherwise styled
     as a destructive red row (.row-menu-dropdown button), which these
     aren't. */
  .view-toggle { display: flex; flex-direction: column; gap: var(--space-0); }
  .view-menu-label {
    padding: var(--space-1) var(--space-2) 0; font-size: var(--text-2xs); font-weight: 700;
    letter-spacing: 0.04em; text-transform: uppercase; color: var(--slate);
  }
  .view-menu-dropdown { min-width: 11rem; text-align: left; }
  .row-menu-dropdown.view-menu-dropdown button { color: var(--ink); white-space: nowrap; }
  @media (hover: hover) { .row-menu-dropdown.view-menu-dropdown button:hover { background: var(--accent-soft); } }
  .view-menu-dropdown button::before { content: ""; display: inline-block; width: 1.1em; }
  .view-menu-dropdown button[aria-pressed="true"]::before { content: "\2713"; }
  .view-menu-dropdown .view-menu-select { border-top: 1px solid var(--rule); border-radius: 0; }

  /* ── The search page's start state (P2-28) ──
     Two columns under the box: the firm's client interests and this
     browser's recent searches, each a list of one-click queries. Rows,
     not chips: an interest row carries its client and a count, and the
     arrow / magnifier say "press this and a search runs". One column
     alone (a guest has no clients; a fresh browser no history) takes
     the full width; below 768px they stack. */
  .start-columns { margin-top: var(--space-5); display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-5) var(--space-6); }
  .start-columns:has(> [hidden]) { grid-template-columns: minmax(0, 1fr); }
  @media (max-width: 767px) { .start-columns { grid-template-columns: minmax(0, 1fr); } }
  .start-col[hidden] { display: none; }
  .start-col-head {
    display: flex; align-items: baseline; gap: var(--space-3);
    padding-bottom: var(--space-3); border-bottom: 1px solid var(--rule);
  }
  .start-col-title { margin: 0; font-size: var(--text-base); font-weight: 600; white-space: nowrap; }
  .start-col-sub { font-size: var(--text-xs); color: var(--slate); }
  .start-col-action { margin-left: auto; font-size: var(--text-sm); color: var(--slate); text-decoration: underline; }
  .start-col-clear { font: inherit; font-size: var(--text-sm); padding: 0; background: none; border: 0; text-decoration: none; cursor: pointer; }
  /* background too, or the base button:hover's fill wins on Clear. */
  @media (hover: hover) { .start-col-action:hover { color: var(--ink); background: none; } }
  .start-list { list-style: none; margin: 0; padding: 0; }
  .start-list > li { border-bottom: 1px solid var(--rule); }
  .start-row {
    display: flex; align-items: center; gap: var(--space-3); width: 100%; min-height: 3rem;
    padding: var(--space-3) var(--space-3); text-align: left; cursor: pointer;
    font: inherit; color: var(--ink); background: transparent; border: 0; border-radius: 0;
  }
  @media (hover: hover) { .start-row:hover { background: var(--accent-soft); color: var(--ink); } }
  .start-row-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
  .start-row-term { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
  .start-row-sub { font-size: var(--text-sm); color: var(--slate); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .start-row-arrow { flex: none; width: 1rem; height: 1rem; color: var(--slate); }
  .start-row-icon { flex: none; width: 1rem; height: 1rem; color: var(--slate); }
  .start-row-wrap { display: flex; align-items: stretch; }
  .start-row-x {
    flex: none; min-width: 2.75rem; padding: 0; font: inherit; font-size: var(--text-md); line-height: 1;
    color: var(--slate); background: transparent; border: 0; border-radius: 0; cursor: pointer;
  }
  @media (hover: hover) { .start-row-x:hover { color: var(--error); background: var(--accent-soft); } }
  .start-tip { margin: var(--space-6) 0 0; font-size: var(--text-sm); color: var(--slate); }
  .start-tip[hidden] { display: none; }
  #saved-slot:not(:empty) { margin-top: var(--space-5); }
  /* Above results the saved searches fold to one line (whole-app audit
     Y13) — a full card there pushed the first result down its height. */
  #saved-slot:has(> .saved-disclosure) { margin-top: var(--space-3); margin-bottom: var(--space-3); }
  .saved-disclosure > summary {
    display: inline-flex; align-items: center; gap: var(--space-1); min-height: 1.5rem;
    cursor: pointer; font-size: var(--text-sm); font-weight: 600; color: var(--slate);
  }
  @media (hover: hover) { .saved-disclosure > summary:hover { color: var(--ink); } }
  .saved-disclosure-n { font-weight: 400; }
  .saved-disclosure-panel { margin-top: var(--space-2); }
  .saved-disclosure-sub { padding: var(--space-3) var(--space-4) 0; }

  /* ── The search bar: one box, the scope dropdowns inside it ──
     The box is the field, not a card around it: magnifier, input, then
     "Search in" and Session as borderless dropdowns past a divider, the
     Search button beside. The input gives up its own pill and glow to
     the box, so focus lights the whole thing. On a phone the dropdowns
     drop to a row of their own under the input. */
  .search-card-query .lookup-query-form { align-items: stretch; flex-wrap: nowrap; gap: var(--space-3); }
  .search-bar-field {
    flex: 1; min-width: 0; min-height: 3.5rem; padding-right: var(--space-3);
    border: 1px solid var(--rule-strong); border-radius: var(--radius-lg); background: var(--surface);
  }
  /* The global 2px gold ring, on the box: the old faint glow was only
     2.03:1 from the resting border (WCAG 2.4.7 / 1.4.11; whole-app
     audit Y26). Same on .search-box and .dir-search-box. */
  .search-bar-field:focus-within {
    border-color: var(--gold);
    outline: 2px solid var(--gold); outline-offset: 2px;
  }
  .search-bar-field .search-field-icon { position: static; margin-left: var(--space-4); width: 1rem; height: 1rem; }
  .search-field.search-bar-field input,
  .search-field.search-bar-field:has(.search-field-clear:not([hidden])) input {
    flex: 1; min-width: 0; align-self: stretch; font-size: var(--text-md);
    padding: 0 var(--space-2) 0 var(--space-3);
    border: 0; border-radius: 0; background: transparent; box-shadow: none;
  }
  /* The box already lights up on focus; the input's own :focus-visible
     outline drew a second, square ring inside the rounded one. */
  .search-field.search-bar-field input:focus-visible { outline: none; }
  .search-bar-field .search-field-clear { position: static; flex: none; }
  .search-scope {
    display: flex; align-items: center; gap: var(--space-1); flex: none;
    margin-left: var(--space-2); padding-left: var(--space-2); border-left: 1px solid var(--rule-strong);
  }
  .search-scope:not(:has(> select:not([hidden]))) { display: none; }
  .search-scope-select[hidden] { display: none; }
  .search-scope-select {
    -webkit-appearance: none; appearance: none; font-size: var(--text-base); color: var(--ink);
    padding: var(--space-1) var(--space-5) var(--space-1) var(--space-2);
    border: 0; border-radius: var(--radius-md); background-color: transparent; box-shadow: none;
    background-image: linear-gradient(45deg, transparent 50%, var(--slate) 50%),
                      linear-gradient(135deg, var(--slate) 50%, transparent 50%);
    background-position: calc(100% - 12px) 55%, calc(100% - 7px) 55%;
    background-size: 5px 5px; background-repeat: no-repeat;
  }
  .search-scope-select:focus { box-shadow: none; }
  @media (hover: hover) { .search-scope-select:hover { background-color: var(--accent-soft); } }
  .search-card-query .lookup-query-form > button[type="submit"] {
    flex: none; min-height: 3.5rem; padding: 0 var(--space-5); border-radius: var(--radius-lg);
    font-size: var(--text-base);
  }
  @media (max-width: 599px) {
    /* The gap under the dropdown row belongs to that row: a bar with
       no dropdowns (or none showing) stays level with its button. */
    .search-bar-field { flex-wrap: wrap; min-height: calc(3.25rem + 2px); }
    .search-field.search-bar-field input { min-height: 3.25rem; flex-basis: calc(100% - 5rem); }
    /* As tall as the whole box, dropdown row included. */
    .search-card-query .lookup-query-form > button[type="submit"] { align-self: stretch; min-height: calc(3.25rem + 2px); }
    .search-scope { flex-basis: 100%; margin-left: 0; padding-left: var(--space-2); border-left: 0; border-top: 1px solid var(--rule); padding-top: var(--space-1); padding-bottom: var(--space-1); }
  }

  /* ── The search page's two columns ──
     The search bar pins to the top of the viewport; beneath it, filters
     in a sticky left column and results to their right, so narrowing a
     long result set never means scrolling back up to reach a control.

     The document scrolls, not .app-main (see .app-main), which is what
     lets anything pin. The bar is a band of page background around the search card,
     so what scrolls beneath it is hidden rather than peeking past the
     card's rounded corners. The sidebar's `top` is the bar's measured
     height (--lookup-bar-h, set by LOOKUP_BODY's ResizeObserver), and it
     scrolls within itself if it outgrows the viewport. Status tips are
     held to the column's width so they never widen that scroll box.

     Two columns need room: below 1100px (a laptop with the app sidebar
     open leaves the results 480px) the filters go back to being the
     horizontal rail above the results. Below 900px nothing pins either:
     a pinned search card is a third of a phone screen. */
  .lookup-search-bar > .search-card-query { margin-bottom: 0; }
  /* relative: the "Skip to results" link inside it is revealed at the
     bar's corner, not the page's, when the bar isn't pinned. */
  .lookup-search-bar { position: relative; margin-bottom: var(--space-4); }
  /* "Finding bills for <client>" under the query row (LOOKUP_BODY's
     renderClientContext) — one wrapping line, not a card. */
  .client-context { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-3); margin-top: var(--space-2); font-size: var(--text-sm); color: var(--slate); }
  .client-context[hidden] { display: none; }
  .client-context strong { color: var(--ink); }
  /* "Bill text: 2 of about 4,200 bills indexed — results are incomplete."
     under the query row while a local index is partial (Oct 7 audit B2). */
  .search-coverage { margin: var(--space-2) 0 0; font-size: var(--text-sm); color: var(--slate); }
  .search-coverage[hidden] { display: none; }
  .lookup-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-3) var(--space-4); align-items: start; }
  .lookup-main { min-width: 0; }
  /* The rail follows the results in the markup (keyboard and screen-reader
     users reach the bills first) but is drawn first: above the results in
     one column, the left track in two. */
  .lookup-layout > .lookup-sidebar:not(.filter-sheet) { order: -1; }
  /* The count is one phrase; the view controls beside it wrap instead. */
  .lookup-main .panel-head > .sub { white-space: nowrap; }
  .lookup-sidebar .search-filters { margin-bottom: 0; }
  /* Below 768px the rail is out of the page and behind the results
     header's Filters button, opening as .filter-sheet (LOOKUP_BODY's
     railHtml): stacked open above the results it pushed the first bill
     two screens down. */
  @media (max-width: 767px) {
    .lookup-sidebar:not(.filter-sheet) { display: none; }
  }
  /* The sheet is the frame; the rail's own card inside it was a second. */
  .lookup-sidebar.filter-sheet .search-filters { border: 0; padding: 0; background: none; }
  @media (min-width: 901px) {
    /* No band above the bar: at rest it sits under the page heading,
       and pinned it meets the window's top edge. */
    .lookup-search-bar {
      position: sticky; top: 0; z-index: 15; background: var(--content-bg);
      padding: 0 0 var(--space-3); margin: 0 0 var(--space-1);
    }
  }
  @media (min-width: 1101px) {
    .lookup-layout:not(.no-rail) { grid-template-columns: 15rem minmax(0, 1fr); }
    .lookup-sidebar {
      position: sticky; top: var(--lookup-bar-h, 0px);
      max-height: calc(100vh - var(--lookup-bar-h, 0px) - var(--space-3)); overflow-y: auto;
    }
    .lookup-sidebar .search-filters { gap: var(--space-4); }
    .lookup-sidebar .facet-groups { gap: var(--space-4); }
    .lookup-sidebar .filter-group { flex-direction: column; align-items: stretch; gap: var(--space-1); }
    .lookup-sidebar .filter-group-label { min-width: 0; }
    .lookup-sidebar .filter-group-head { padding-right: 0; border-right: 0; }
    .lookup-sidebar .filter-group-head .filter-group-label { min-width: 0; }
    .lookup-sidebar .filter-group .filter-tab { display: flex; align-items: center; text-align: left; white-space: normal; }
    .lookup-sidebar .filter-group .filter-tab .n { margin-left: auto; padding-left: var(--space-2); }
    .lookup-sidebar .has-tip > .tip { max-width: 100%; }
  }

  /* ── Search filters (P1-11) ──
     The rail above the results. Every dimension here is DERIVED from a
     field getSearch already returns (see _shape_search_rows in
     legiscan_client.py) — chamber and measure type off the bill number's
     prefix, session off the result's own LegiScan URL, status off the
     last-action text via bill_status.js. Nothing here costs an API call,
     which is why filtering a 150-row result set is affordable at all.

     Author and committee are deliberately absent: getSearch carries
     neither, and the only way to know them is a getBill per row. Same
     house rule as the 601's "Known gaps" card — leave out what we can't
     know rather than guess it. */
  .search-filters {
    display: flex; flex-direction: column; gap: var(--space-3);
    background: var(--surface); border: 1px solid var(--rule);
    border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-3);
  }
  .filter-group { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
  /* The three client-side facet groups flow together and wrap as units,
     instead of each claiming a full row of its own. A wider column-gap
     than the gap inside a group keeps the groups reading as separate
     things once two share a line. */
  /* One row per facet group, labels in a left column — groups flowing
     together read as one run of pills with no clear boundaries. */
  .facet-groups { display: flex; flex-direction: column; gap: var(--space-2); }
  /* A group label with a shortcut beside it — Status's "Active only",
     which is a preset over that group's own tabs. */
  .filter-group-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
  /* In a wrapping row the preset would read as one more status; a rule
     after it sets it apart. The sidebar's column layout doesn't need it. */
  .filter-group-head { padding-right: var(--space-2); border-right: 1px solid var(--rule); }
  .filter-group-head .filter-group-label { min-width: 5.5rem; }
  .save-search-open, .result-controls .linklike { font-size: var(--text-xs); }
  .save-search-open { padding: var(--space-1) var(--space-3); }
  /* Opens beneath the results header, spanning it. */
  .save-search-form {
    display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; font-size: var(--text-xs);
    margin: 0 0 var(--space-3); padding-bottom: var(--space-3); border-bottom: 1px solid var(--rule);
  }
  .save-search-form label { display: inline-flex; align-items: center; gap: var(--space-1); color: var(--slate); }
  .save-search-note { flex-basis: 100%; margin: 0; }
  .save-search-form input { padding: var(--space-1) var(--space-2); font-size: var(--text-xs); }
  /* At phone width the inline label + field pairs wrapped "Save for
     client:" onto three lines beside a select wider than the panel. Each
     label goes above its own full-width field instead. */
  @media (max-width: 767px) {
    .save-search-form label { flex: 1 1 100%; flex-direction: column; align-items: stretch; min-width: 0; }
    .save-search-form input, .save-search-form select { width: 100%; min-width: 0; }
  }
  /* min-height: the uppercase label alone made an 18px target (WCAG
     2.5.8 wants 24px; see .linklike's standalone rule). */
  .filter-group-toggle {
    background: none; border: 0; padding: 0; cursor: pointer; text-align: left;
    display: inline-flex; align-items: center; gap: var(--space-1); min-height: 1.5rem;
  }
  .filter-group-toggle::after { content: "\25B8"; font-size: var(--text-2xs); }
  .filter-group-toggle[aria-expanded="true"]::after { content: "\25BE"; }
  .filter-group-folded { margin: 0; font-size: var(--text-xs); }
  .filter-group-label {
    font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--slate); min-width: 5.5rem;
  }
  .filter-group .filter-tab { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); }
  /* A filter that would empty the list is shown, disabled, with its zero
     visible — "no Senate bills here" is an answer, and a control that
     silently vanishes teaches the user the filter is unreliable.

     Which is exactly why this can't be a blanket opacity: 0.45 over
     --slate measured 1.94:1, and a count nobody can read is not an
     answer either. WCAG's exemption for inactive controls doesn't cover
     a disabled control being used to carry information. The border
     fades instead — it's decoration here — and the text stays at
     --slate's 5.8:1. "Off" now reads from the flat, borderless shape
     and the zero, not from the text being faint. */
  .filter-tab[disabled] {
    cursor: default; color: var(--slate);
    background: transparent; border-color: var(--rule);
  }
  /* The zero-count facet tab: same look, but aria-disabled so it stays
     focusable and announced rather than skipped. */
  .filter-tab[aria-disabled="true"] { cursor: default; color: var(--slate); background: transparent; border-color: var(--rule); }
  @media (hover: hover) { .filter-tab[disabled]:hover, .filter-tab[aria-disabled="true"]:hover { border-color: var(--rule); } }
  /* The results panel head's right cluster: how you view the set (view,
     Sort, Select all) and what to do with the whole search (Save). What
     is IN the set — Active only, Hide N tracked — is the rail's; here
     they read as view switches that quietly moved the rail's status
     tabs (search/dashboard audit S9). */
  .result-controls { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; justify-content: flex-end; margin-left: auto; }
  .result-controls .filter-tab { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); }
  .view-sort {
    display: inline-flex; align-items: center; gap: var(--space-1);
    font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--slate);
  }
  .view-sort select {
    font: inherit; font-size: var(--text-xs); font-weight: 400; letter-spacing: 0; text-transform: none;
    padding: var(--space-1) var(--space-2);
  }
  /* A phone's select is 16px text (no zoom on focus), so Sort is as wide
     as its longest option and took a row between Filters and View. It
     goes last, on a row of its own: Filters, View and Save share one. */
  @media (max-width: 767px) {
    .result-controls .view-sort { order: 1; flex: 1 1 100%; justify-content: flex-end; }
  }

  /* Active filters restated as removable chips. The tabs above already
     show their own state, but once three groups are set the only way to
     see everything that is narrowing the list — without reading four
     rows of controls — is one line that says so. */
  .active-filters {
    display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
    border-top: 1px solid var(--rule); padding-top: var(--space-3);
  }
  /* border-box: the top padding plus one chip's height, so the row is
     the same height empty and filled. */
  .active-filters { min-height: calc(var(--space-3) + 1.7rem); }
  .active-filters.is-empty .sub { margin: 0; font-size: var(--text-xs); }
  .filter-chip {
    font: inherit; font-size: var(--text-xs); font-weight: 600; cursor: pointer;
    display: inline-flex; align-items: center; gap: var(--space-1);
    background: var(--accent-soft); color: var(--accent); border: none;
    border-radius: var(--radius-pill); padding: var(--space-1) var(--space-2);
  }
  @media (hover: hover) { .filter-chip:hover { background: var(--accent-solid); color: var(--accent-solid-text); } }
  .filter-chip .x { font-size: var(--text-sm); line-height: 1; }
  .filter-clear { font-size: var(--text-xs); }

  /* ── Bill search v5 (docs/design-system) ──
     Laid over the search rules above, scoped to .lookup-page since the
     filter tabs, chips, rail, saved-search rows, status pills and the
     search field are shared with Flagged bills, Lobbying, Alerts and a
     client's page. Serif page and card titles; white 18px cards on a
     hairline with no shadow and no header rule; result, start-state and
     saved-search rows inset with their own 12px corners, tinting on
     hover (CardRow); labels and column heads in sentence case; bill
     numbers mono navy before the title (BillNumber); filter chips
     borderless neutral pills (FilterChip); a picked filter tinted navy
     (FilterDropdown); status the Flagged list's white StatusDot pill.
     Buttons and fields keep the app's own shape. */
  .lookup-page .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em;
  }
  .lookup-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }

  /* Cards: the results, the saved searches and the filter rail. */
  .lookup-page .panel { border-color: var(--rule); box-shadow: none; }
  .lookup-page .panel-head { border-bottom: 0; }
  .lookup-page .panel-head .title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; letter-spacing: -0.01em;
  }
  .lookup-page .panel-head .sub { color: var(--ink-2); }
  .lookup-page .search-filters { border-color: var(--rule); border-radius: var(--radius-lg); }
  .lookup-page .filter-sheet-head .title { font-family: var(--font-display); font-weight: 500; }

  /* Result rows: inset 4px from the card's edge, their text level with
     the head's; no border of their own, a tint on hover. */
  .lookup-page .results-panel > .result-cards { margin-inline: var(--space-1); gap: var(--space-0); }
  .lookup-page .result-card { border: 0; border-radius: var(--radius-md); padding: var(--space-3); }
  @media (hover: hover) { .lookup-page .result-card:hover { background: var(--paper-50); box-shadow: none; } }
  .lookup-page .result-card-number, .lookup-page .result-table td.col-bill a {
    font-family: var(--mono); font-size: var(--text-xs); font-weight: 400; letter-spacing: 0; color: var(--link);
  }
  .lookup-page .result-card-title { font-size: var(--text-sm); font-weight: 500; }
  /* Links go to ink on hover: the title underlines, the number stays navy. */
  @media (hover: hover) {
    .lookup-page .result-card-number:hover,
    .lookup-page .result-card-link:hover .result-card-number,
    .lookup-page .result-table td.col-bill a:hover { color: var(--link); }
    .lookup-page .result-card-title:hover,
    .lookup-page .result-card-link:hover .result-card-title { color: var(--ink); text-decoration: underline; }
  }
  .lookup-page .result-card-meta { color: var(--ink-2); }
  .lookup-page .result-card-meta strong { font-weight: 500; }
  .lookup-page .result-group-head { font-weight: 500; }
  .lookup-page .result-group-head.is-text-only h3 { font-weight: 500; }

  /* StatusDot pill: a white pill on a hairline, the word in --ink-2, the
     stage glyph alone carrying the stage's colour (as on Flagged bills). */
  .lookup-page .status-badge[class*="tone-"] { background: var(--surface); border-color: var(--rule); color: var(--ink-2); }
  .lookup-page .status-badge.tone-early::before { color: var(--muted); }
  .lookup-page .status-badge.tone-moving::before { color: var(--link); }
  .lookup-page .status-badge.tone-passed::before { color: var(--good); }
  .lookup-page .status-badge.tone-ended::before { color: var(--error); }
  .lookup-page .status-legend-head { text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--muted); }

  /* The compact table: a tinted, rounded head band in sentence case,
     inset from the card's edge; rows tint on hover. */
  .lookup-page .result-table-wrap { padding: 0 var(--space-2); }
  .lookup-page .result-table { border-collapse: separate; border-spacing: 0; }
  .lookup-page .result-table th {
    padding-top: var(--space-2); border-bottom: 0; background: var(--paper-50);
    font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
  }
  .lookup-page .result-table th:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
  .lookup-page .result-table th:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
  .lookup-page .result-table th:first-child, .lookup-page .result-table td:first-child { padding-left: var(--space-3); }
  .lookup-page .result-table tbody tr:last-child td { border-bottom: 0; }
  @media (hover: hover) { .lookup-page .result-table tbody tr:hover td { background: var(--paper-50); } }

  /* The header's Sort and View menu labels, in sentence case. */
  .lookup-page .view-sort, .lookup-page .view-menu-label {
    font-size: var(--text-xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--ink-2);
  }

  /* The filter rail: sentence-case group labels; a picked filter tints
     navy and keeps its leading check, so it never rests on colour. */
  .lookup-page .filter-group-label { font-size: var(--text-xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted); }
  .lookup-page .filter-tab { font-weight: 500; }
  .lookup-page .filter-tab.active:not([disabled]):not([aria-disabled="true"]) { background: var(--info-soft); border-color: var(--link); color: var(--link); }
  @media (hover: hover) {
    .lookup-page .filter-tab.active:not([disabled]):not([aria-disabled="true"]):hover { background: var(--info-soft); color: var(--link); }
  }
  /* FilterChip: a borderless neutral pill with its ×; "Clear all" a link. */
  .lookup-page .filter-chip { background: var(--paper-250); color: var(--ink-2); font-weight: 500; }
  @media (hover: hover) { .lookup-page .filter-chip:hover { background: var(--paper-350); color: var(--ink); } }
  .lookup-page .filter-chip .x { color: var(--muted); }
  .lookup-page .filter-clear { font-size: var(--text-xs); font-weight: 500; color: var(--link); text-decoration: none; }
  @media (hover: hover) { .lookup-page .filter-clear:hover { color: var(--ink); } }

  /* The start state: each column a card with a serif title and inset
     rows; "Edit in Clients →" and "Clear" muted text actions. Each card
     holds to its own rows rather than stretching to its neighbour's. */
  .lookup-page .start-columns { align-items: start; }
  .lookup-page .start-col {
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius-lg); padding: var(--space-1);
  }
  .lookup-page .start-col-head { padding: var(--space-2) var(--space-3) var(--space-1); border-bottom: 0; }
  .lookup-page .start-col-title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; letter-spacing: -0.01em;
  }
  .lookup-page .start-col-sub { color: var(--ink-2); }
  .lookup-page .start-col-action { font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); text-decoration: none; }
  .lookup-page .start-list > li { border-bottom: 0; }
  .lookup-page .start-row, .lookup-page .start-row-x { border-radius: var(--radius-md); }
  .lookup-page .start-row-term { font-weight: 500; }
  .lookup-page .start-row-sub { font-size: var(--text-xs); color: var(--ink-2); }
  @media (hover: hover) {
    .lookup-page .start-row:hover { background: var(--paper-50); }
    .lookup-page .start-row-x:hover { background: var(--paper-50); }
  }
  .lookup-page .start-tip { color: var(--ink-2); }

  /* Saved searches: a card of inset rows; "N new" the accent count pill. */
  .lookup-page #saved-panel.panel, .lookup-page .saved-disclosure-panel { padding: var(--space-1); }
  .lookup-page #saved-panel .panel-head { padding: var(--space-2) var(--space-3) var(--space-1); }
  .lookup-page .saved-disclosure-sub { padding: var(--space-2) var(--space-3) var(--space-1); color: var(--ink-2); }
  .lookup-page .saved-disclosure > summary { font-weight: 500; color: var(--ink-2); }
  .lookup-page #saved-list .saved-row { border-bottom: 0; border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); align-items: center; }
  @media (hover: hover) { .lookup-page #saved-list .saved-row:hover { background: var(--paper-50); } }
  .lookup-page .saved-name { font-weight: 500; }
  @media (hover: hover) { .lookup-page .saved-name:hover { color: var(--ink); } }
  .lookup-page .saved-meta { font-size: var(--text-xs); color: var(--ink-2); }
  .lookup-page .unread-dot {
    height: auto; padding: 0 var(--space-2); background: var(--info-soft); color: var(--link);
    font-weight: 500; line-height: 1.4;
  }

  @media (max-width: 640px) {
    .lookup-page .page-head h1 { font-size: var(--text-xl); }
  }
  /* On a phone the box beside Search is ~210px wide, and "Search in"
     and Session side by side are ~330px: the Session dropdown ran out
     past the box's edge. They wrap to a line each instead. */
  @media (max-width: 599px) {
    .lookup-page .search-scope { flex-wrap: wrap; }
    .lookup-page .search-scope-select { max-width: 100%; }
  }

  /* ── Flagged list: views, grouping, export (P2-24) ──
     The rail above reuses .search-filters/.filter-group/.filter-tab
     wholesale — same component, second page. Only what the flagged list
     adds is here. */

  /* A saved view is a filter composition under a name, so it reads as a
     filter tab with a delete beside it rather than as a list item — it
     does the same thing the tabs above do when clicked. */
  .saved-views { gap: var(--space-3); }

  /* The flagged list's one filter bar (audit S2): the text filter, a
     dropdown per dimension, then the active chips and saved views, all
     flowing on one line inside the results panel between its controls
     and its rows. The chips and views lose the rule .active-filters
     draws on /lookup — here they're further items on the same bar. */
  .filter-bar {
    display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-3);
    padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--rule);
  }
  .filter-bar-rail { display: contents; }
  .filter-bar .active-filters { border-top: 0; padding-top: 0; }
  .filter-dds { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); }
  .filter-dd { position: relative; }
  .filter-dd-btn, .filter-sheet-btn { display: inline-flex; align-items: center; gap: var(--space-0); }
  .filter-dd-btn .caret { margin-left: var(--space-1); width: 0.625rem; height: 0.625rem; flex: none; }
  .filter-sheet-btn { display: none; }
  /* The same anchored surface as .row-menu-dropdown (see row_menu.js),
     opening left-aligned under its button since the bar reads from the
     left. max-height is set per open by fitOpenPopover(). */
  .filter-pop {
    position: absolute; left: 0; top: calc(100% + 0.25rem); background: var(--surface);
    border: 1px solid var(--rule-card); border-radius: var(--radius-md); padding: var(--space-1);
    min-width: 13rem; max-width: 20rem; max-height: 18rem; overflow-y: auto;
    box-shadow: var(--shadow-pop); z-index: 20;
    display: flex; flex-direction: column;
  }
  .filter-pop[hidden] { display: none; }
  .filter-option {
    display: flex; align-items: center; gap: var(--space-2); cursor: pointer;
    font-size: var(--text-xs); color: var(--ink); padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
  }
  @media (hover: hover) { .filter-option:hover { background: var(--accent-soft); } }
  .filter-option input { margin: 0; flex: none; accent-color: var(--accent-solid); }
  .filter-option-label { flex: 1; }
  .filter-option .n { font-variant-numeric: tabular-nums; font-size: var(--text-xs); color: var(--slate); }
  /* A value that would empty the list, shown with its zero — same rule
     as .filter-tab[disabled]: no opacity, the text stays at --slate. */
  .filter-option.off { cursor: default; color: var(--slate); }
  @media (hover: hover) { .filter-option.off:hover { background: none; } }

  /* Below 768px the dropdowns fold into one "Filters" button and a
     bottom sheet. Fixed to the viewport's bottom edge, over a scrim, at
     most 85% of the screen so the page behind still reads as behind. */
  .view-controls { display: flex; align-items: flex-start; gap: var(--space-2) var(--space-4); flex-wrap: wrap; }
  @media (max-width: 767px) {
    .filter-dds .filter-dd { display: none; }
    .filter-sheet-btn { display: inline-flex; }
    /* The sheet holds these at this width; the bar keeps the text filter
       and the Filters button side by side, then the chips. */
    .filter-bar .saved-views, .panel-head > .view-controls { display: none; }
    .filter-bar .search-box { flex: 1 1 8rem; width: auto; }
  }
  .sheet-scrim { position: fixed; inset: 0; background: var(--scrim); z-index: 99; }
  .sheet-scrim[hidden] { display: none; }
  .filter-sheet {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 100; max-height: 85vh;
    display: flex; flex-direction: column; background: var(--surface);
    border-top: 1px solid var(--rule-card); border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: var(--shadow-hover);
  }
  .filter-sheet[hidden] { display: none; }
  .filter-sheet-head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    padding: var(--space-4); border-bottom: 1px solid var(--rule);
  }
  .filter-sheet-head .title { font-size: var(--text-base); font-weight: 700; margin: 0; }
  .filter-sheet-scroll { overflow-y: auto; padding: var(--space-2) var(--space-4); }
  .filter-sheet-view .view-controls { flex-direction: column; align-items: stretch; margin-top: var(--space-2); }
  .filter-sheet-group { border: 0; margin: 0; padding: var(--space-2) 0; }
  .filter-sheet-group + .filter-sheet-group, #filter-sheet-body:not(:empty) + .filter-sheet-view { border-top: 1px solid var(--rule); }
  .filter-sheet-group legend { padding: var(--space-2) 0; }
  .filter-sheet .filter-option { min-height: 2.75rem; font-size: var(--text-sm); }
  .filter-sheet-foot { padding: var(--space-3) var(--space-4); border-top: 1px solid var(--rule); }
  .filter-sheet-foot button { width: 100%; min-height: 2.75rem; }
  .saved-view { display: inline-flex; align-items: center; gap: var(--space-0); }
  .saved-view .icon-btn { font-size: var(--text-sm); padding: 0 var(--space-1); }

  /* Sort by: the design system's Select — the label muted to its left,
     a 30px field with the control radius rather than the app's pill. */
  .sort-by {
    display: flex; align-items: center; gap: var(--space-2); white-space: nowrap;
    font-size: var(--text-xs); color: var(--ink-2);
  }
  .sort-by select {
    min-height: 1.875rem; padding: 0 var(--space-2); border-radius: var(--radius-sm);
    font-size: var(--text-xs); color: var(--ink); box-shadow: var(--shadow-rest);
  }
  /* Checkbox: navy when checked (design system Checkbox), the label in
     ink at the same 30px line as the select beside it. */
  .group-toggle {
    display: flex; align-items: center; gap: var(--space-2); min-height: 1.875rem;
    font-size: var(--text-xs); color: var(--ink); cursor: pointer; white-space: nowrap;
  }
  .group-toggle input { margin: 0; accent-color: var(--accent); }
  /* The checkbox with its one-line explanation directly under it
     (whole-app audit Y4), rather than the hint trailing the whole row. */
  .group-toggle-wrap { display: flex; flex-direction: column; gap: var(--space-0); }
  .group-toggle-wrap .control-hint { margin: 0; font-size: var(--text-2xs); color: var(--muted); }
  /* Export as quiet text: the least-used control in the row, it used to
     be its only bordered button. Same 24px target as .panel-head
     .linklike, also once the controls move into the phone filter sheet.
     The design system's link button: navy, no underline, ink on hover. */
  .view-controls .export-link {
    display: inline-flex; align-items: center; min-height: 1.5rem;
    font-size: var(--text-xs); white-space: nowrap;
    color: var(--link); font-weight: 500; text-decoration: none;
    margin-top: var(--space-0);   /* centred on the 30px select beside it */
  }
  @media (hover: hover) { .view-controls .export-link:hover { color: var(--ink); } }

  /* One section per client. The heading is a band rather than a table
     caption because each section is its own scrollable table — the
     grouped view is really N small tables, so the client's name has to
     sit outside the overflow container to stay put while a wide row
     scrolls under it. */
  .row-group { border-bottom: 1px solid var(--rule); }
  .row-group:last-child { border-bottom: 0; }
  .row-group-head {
    display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3);
    padding: var(--space-3) var(--space-4) var(--space-2);
    background: var(--accent-soft);
  }
  .row-group-name { font-size: var(--text-sm); font-weight: 700; }
  .row-group-count { font-size: var(--text-2xs); color: var(--slate); }

  /* ── Flagged bills v5 (docs/design-system: Card, data, forms, actions) ──
     Laid over the rules above for this page only: .flagged-card is the
     results panel. A flush Card — white, 18px, hairline edge, no shadow,
     a serif title, no rule under the head — holding a 36px text filter
     and 30px filter dropdowns, then a table whose head is a tinted,
     rounded band in sentence case and whose rows tint on hover. Bill
     numbers are mono navy over an ink title. Shapes and tones the app
     keeps on purpose stay: the pills' one base shape and their borders
     and glyphs (P2-31), the --gold focus ring, the 44px touch floor. */
  .panel.flagged-card { border-color: var(--rule); box-shadow: none; }
  .flagged-card .panel-head { align-items: flex-start; border-bottom: 0; padding: var(--space-4) var(--space-4) var(--space-3); }
  .flagged-card .panel-head .title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; letter-spacing: -0.01em;
  }
  .flagged-card .panel-head .sub { color: var(--ink-2); }
  .flagged-card .panel-head .linklike { color: var(--link); font-weight: 500; text-decoration: none; }
  @media (hover: hover) { .flagged-card .panel-head .linklike:hover { color: var(--ink); } }

  /* The filter bar sits under the head with no rule of its own; the
     table's tinted head band is the divider. */
  .flagged-card .filter-bar { border-bottom: 0; padding: 0 var(--space-4) var(--space-3); }
  /* TextInput: 36px, control radius, white field on a control edge. */
  .flagged-card .search-box {
    height: 2.25rem; border-radius: var(--radius-sm);
    background: var(--surface); box-shadow: var(--shadow-rest); color: var(--muted);
  }
  /* Below 768px the phone rules above size it to the row. */
  @media (min-width: 768px) { .flagged-card .search-box { width: 15rem; } }
  .flagged-card .search-box input { font-size: var(--text-sm); }
  .flagged-card .search-box svg { width: 0.8rem; height: 0.8rem; }
  /* FilterDropdown: 30px, control radius, quiet at rest; with anything
     picked it tints navy and the count sits in a navy pill. The count is
     the non-colour cue, so the active tab's leading check is dropped. */
  :is(.flagged-card, .alerts-page) .filter-tab {
    min-height: 1.875rem; padding: 0 var(--space-2) 0 var(--space-3); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--ink-2); font-weight: 400; box-shadow: var(--shadow-rest);
  }
  @media (hover: hover) { :is(.flagged-card, .alerts-page) .filter-tab:hover { border-color: var(--ink); color: var(--ink); } }
  :is(.flagged-card, .alerts-page) .filter-tab.active { background: var(--info-soft); border-color: var(--link); color: var(--link); font-weight: 500; }
  @media (hover: hover) {
    :is(.flagged-card, .alerts-page) .filter-tab.active:not([disabled]):not([aria-disabled="true"]):hover { background: var(--info-soft); color: var(--link); }
  }
  :is(.flagged-card, .alerts-page) .filter-dd-btn.active::before, :is(.flagged-card, .alerts-page) .filter-sheet-btn.active::before { content: none; }
  :is(.flagged-card, .alerts-page) .filter-dd-btn .n, :is(.flagged-card, .alerts-page) .filter-sheet-btn .n {
    margin-left: var(--space-1); padding: 0 var(--space-1); border-radius: var(--radius-pill);
    background: var(--link); color: var(--surface); font-size: var(--text-2xs); font-weight: 600; line-height: 1.4;
  }
  :is(.flagged-card, .alerts-page) .filter-pop { border-radius: var(--radius-md); }
  @media (hover: hover) { :is(.flagged-card, .alerts-page) .filter-option:hover { background: var(--paper-50); } }
  :is(.flagged-card, .alerts-page) .filter-option input, #filter-sheet .filter-option input { accent-color: var(--accent); }
  :is(.flagged-card, .alerts-page) .filter-option .n { color: var(--muted); }
  /* FilterChip: a borderless neutral pill with its ×; "Clear all" and
     "Save view" are link buttons. */
  :is(.flagged-card, .alerts-page) .filter-chip {
    min-height: 1.625rem; padding: 0 var(--space-2) 0 var(--space-3);
    background: var(--paper-250); color: var(--ink-2); font-weight: 500;
  }
  @media (hover: hover) { :is(.flagged-card, .alerts-page) .filter-chip:hover { background: var(--paper-350); color: var(--ink); } }
  :is(.flagged-card, .alerts-page) .filter-chip .x { color: var(--muted); }
  :is(.flagged-card, .alerts-page) .filter-clear {
    background: none; border-color: transparent; padding: 0 var(--space-1);
    color: var(--link); font-weight: 500; font-size: var(--text-xs);
  }
  @media (hover: hover) { :is(.flagged-card, .alerts-page) .filter-clear:hover { background: none; border-color: transparent; color: var(--ink); } }
  :is(.flagged-card, .alerts-page) .filter-group-label, #filter-sheet .filter-group-label {
    text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--muted);
  }

  /* The table: inset from the card edge, a tinted rounded head band in
     sentence case, hairline row dividers, a row tint on hover. */
  .flagged-card .bill-ident { font-weight: 400; color: var(--link); }
  .flagged-card .bill-name { font-size: var(--text-sm); font-weight: 500; color: var(--ink); }
  @media (hover: hover) {
    .flagged-card a.bill-link:hover .bill-ident { color: var(--link); }
    .flagged-card a.bill-link:hover .bill-name { color: var(--ink); text-decoration: underline; }
  }
  /* CountPill, accent tone: "1 new" on the navy tint, in the UI face. */
  .flagged-card .unread-dot {
    height: auto; padding: 0 var(--space-2); background: var(--info-soft); color: var(--link);
    font-family: var(--font-sans); font-weight: 500; line-height: 1.4;
  }
  .flagged-card td.col-author { color: var(--ink-2); font-size: var(--text-xs); }
  /* Fit at ~1080px (UX review): the Client column is how people tell
     bills apart, so it never goes. Author folds away below 1280px (the
     bill's own page has it), rows are tighter, and each client reads as
     one compact chip. */
  .flagged-card .flagged-table td { padding-block: var(--space-2); }
  .flagged-card td.next-action { min-width: 7rem; }
  .flagged-card .clients-summary-list { gap: var(--space-1); }
  .flagged-card .client-position {
    gap: var(--space-1); padding: 0 var(--space-2); border: 1px solid var(--rule);
    border-radius: var(--radius-pill); background: var(--surface);
  }
  .flagged-card .client-position { max-width: 100%; min-width: 0; }
  .flagged-card .clients-summary, .flagged-card .clients-summary-list { min-width: 0; max-width: 100%; }
  /* max-width:0 + a percentage width makes the cell take its share of the
     table instead of its content's natural width, so long names clip. */
  @media (min-width: 901px) {
    .flagged-card .flagged-table td.col-clients { width: 38%; max-width: 0; }
  }
  .flagged-card .client-position-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  @media (min-width: 901px) and (max-width: 1279px) {
    .flagged-card .flagged-table :is(th, td).col-author { display: none; }
    .flagged-card .flagged-table th.col-clients { min-width: 12rem; }
  }
  .flagged-card .next-when { font-weight: 500; }
  .flagged-card .next-where { color: var(--muted); }
  /* StatusDot pill: a white pill on a hairline, the word in --ink-2, the
     stage glyph alone carrying the stage's colour. */
  .flagged-card .status-badge[class*="tone-"] { background: var(--surface); border-color: var(--rule); color: var(--ink-2); }
  .flagged-card .status-badge.tone-early::before { color: var(--muted); }
  .flagged-card .status-badge.tone-moving::before { color: var(--link); }
  .flagged-card .status-badge.tone-passed::before { color: var(--good); }
  .flagged-card .status-badge.tone-ended::before { color: var(--error); }
  .flagged-card .client-position-name { font-weight: 500; }
  @media (hover: hover) { .flagged-card .clients-summary:hover { background: var(--paper-50); } }
  .flagged-card .clients-summary[aria-expanded="true"] { background: var(--paper-50); }
  .flagged-card .status-legend-head { text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--muted); }
  @media (min-width: 901px) {
    .flagged-card .table-scroll { padding: 0 var(--space-2); }
    .flagged-card .flagged-table { border-collapse: separate; border-spacing: 0; }
    /* The bill takes the room the design gives it (its widest column);
       Clients & positions keeps its 15rem floor from the rules above. */
    .flagged-card .flagged-table th.col-bill { min-width: 13rem; }
    .flagged-card .flagged-table th {
      border-top: 0; border-bottom: 0; background: var(--paper-50);
      font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
    }
    .flagged-card .flagged-table th:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
    .flagged-card .flagged-table th:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
    .flagged-card .flagged-table th:first-child, .flagged-card .flagged-table td:first-child { padding-left: var(--space-3); }
    .flagged-card .flagged-table th:last-child, .flagged-card .flagged-table td:last-child { padding-right: var(--space-2); }
    .flagged-card .flagged-table td { padding-top: var(--space-3); padding-bottom: var(--space-3); }
    .flagged-card .flagged-table tbody tr:last-child td { border-bottom: 0; }
    @media (hover: hover) { .flagged-card .flagged-table tbody tr:hover td { background: var(--paper-50); } }
  }
  /* The phone card's cell labels, in sentence case like the column heads. */
  .flagged-card .bill-cards td::before { text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--muted); }
  /* At 375px a long status ("Died — session deadline") took the whole
     right column and laid its pill over the end of the next-action date
     ("1:30 PM" hidden). The date keeps the room it needs; a long status
     wraps inside its pill instead. */
  @media (max-width: 900px) {
    .flagged-card .bill-cards tr { grid-template-columns: minmax(min-content, 1fr) auto; }
    .flagged-card .bill-cards td.col-status .status-badge { white-space: normal; text-align: left; }
  }
  /* On a phone the status takes its own line under the next action, so a
     long one ("Died — session deadline") sits on one line in its pill
     rather than squeezing into three beside the date. */
  @media (max-width: 640px) {
    .flagged-card .bill-cards td.col-status { grid-column: 1 / -1; grid-row: auto; justify-self: start; }
  }

  /* Group by client: a LetterTile, the client's name in the serif, and
     a count pill — no band, no rule between the groups. */
  .flagged-card .row-group, .flagged-card .row-group:last-child { border-bottom: 0; }
  .flagged-card .row-group-head {
    align-items: center; justify-content: flex-start; gap: var(--space-2);
    padding: var(--space-4) var(--space-4) var(--space-2); background: none;
  }
  .flagged-card .row-group-head .letter-tile { width: 1.5rem; height: 1.5rem; }
  .flagged-card .row-group-name { font-family: var(--font-display); font-size: var(--text-base); font-weight: 500; }
  .flagged-card .row-group-count {
    padding: var(--space-0) var(--space-2); border-radius: var(--radius-pill);
    background: var(--paper-250); color: var(--ink-2); font-weight: 500;
  }

  /* The phone filter sheet's title in the card serif. */
  #filter-sheet .filter-sheet-head .title { font-family: var(--font-display); font-weight: 500; }
  /* The 30px sizes above are pointer sizes: on a touch screen every one
     of these controls keeps the app's 44px floor (see the coarse-pointer
     block by .row-menu-btn), which their extra class would otherwise beat. */
  @media (pointer: coarse) {
    .flagged-card .filter-tab, .flagged-card .filter-chip, .flagged-card .filter-clear, .sort-by select { min-height: 2.75rem; }
  }

  /* ── Bill sub-pages v5 (docs/design-system: Card, data, forms, actions) ──
     The pages that hang off Flagged bills — Archived, Sponsors & votes,
     Whip count, Import a bill list and New session — laid over the
     shared rules for these pages only (.bill-subpage on each page's
     wrapper). Serif page and card titles; white 18px cards on a hairline
     with no shadow and no rule under the head, their content inset from
     the edge; tables with a tinted, rounded head band in sentence case
     and rows that tint on hover, each row a labelled card on a phone
     rather than a sideways scroller hiding columns; bill numbers mono
     navy before their titles (BillNumber). Buttons and fields keep the
     app's own shape; position badges keep their borders and glyphs
     (P2-31); the --gold focus ring and 44px touch floor stay. */
  .bill-subpage .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em; text-wrap: balance; overflow-wrap: anywhere;
  }
  .bill-subpage .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }
  .bill-subpage .page-head a.sub { margin-top: 0; font-size: var(--text-xs); font-weight: 500; }
  @media (hover: hover) { .bill-subpage .page-head a.sub:hover { color: var(--ink); } }
  .bill-subpage .page-head-more > summary { color: var(--ink-2); }
  .whip-page .whip-bill-line { color: var(--ink); font-weight: 500; }
  /* BillNumber: mono navy, regular weight, a small gap before the title. */
  .bill-subpage .subpage-bill-number {
    font-family: var(--mono); font-size: var(--text-xs); font-weight: 400; color: var(--link); white-space: nowrap;
  }

  /* Cards: hairline edge, no shadow, serif title, no header rule. */
  .bill-subpage .panel { border-color: var(--rule); box-shadow: none; }
  .bill-subpage .panel + .panel { margin-top: var(--space-4); }
  .bill-subpage .subpage-stack { display: grid; gap: var(--space-4); }
  .bill-subpage .subpage-stack > .panel + .panel { margin-top: 0; }
  .bill-subpage .panel-head { border-bottom: 0; padding: var(--space-4) var(--space-4) var(--space-3); }
  .bill-subpage .panel-head .title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; line-height: 1.2; letter-spacing: -0.01em;
  }
  .bill-subpage .panel-head .sub { color: var(--ink-2); }
  .bill-subpage .subpage-body { padding: 0 var(--space-4) var(--space-4); }
  .bill-subpage .subpage-body:empty { display: none; }
  .bill-subpage .subpage-body .sub, .bill-subpage .subpage-note { margin: 0 0 var(--space-3); color: var(--ink-2); }
  .bill-subpage .subpage-body > :first-child { margin-top: 0; }
  .bill-subpage .subpage-body > :last-child { margin-bottom: 0; }
  .bill-subpage .subpage-note { padding: 0 var(--space-4); }
  .bill-subpage .empty { color: var(--ink-2); }
  /* Field labels (TextInput / TextArea): 12px, medium, secondary ink. */
  .bill-subpage .field-label { font-weight: 500; color: var(--ink-2); }

  /* StatusDot pill: white on a hairline, the stage glyph alone coloured. */
  .bill-subpage .status-badge[class*="tone-"] { background: var(--surface); border-color: var(--rule); color: var(--ink-2); }
  .bill-subpage .status-badge.tone-early::before { color: var(--muted); }
  .bill-subpage .status-badge.tone-moving::before { color: var(--link); }
  .bill-subpage .status-badge.tone-passed::before { color: var(--good); }
  .bill-subpage .status-badge.tone-ended::before { color: var(--error); }

  /* Tables: sentence-case heads; on a wide screen inset from the card
     edge with a tinted rounded head band and a row tint on hover. */
  .bill-subpage .subpage-table th { font-size: var(--text-xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted); }
  .bill-subpage .subpage-table td.sub, .bill-subpage .subpage-table td .sub { color: var(--ink-2); }
  @media (min-width: 901px) {
    .bill-subpage .subpage-rows { padding: 0 var(--space-2) var(--space-2); }
    .bill-subpage .subpage-table { border-collapse: separate; border-spacing: 0; }
    .bill-subpage .subpage-table th {
      border-top: 0; border-bottom: 0; background: var(--paper-50); padding-top: var(--space-2); padding-bottom: var(--space-2);
    }
    .bill-subpage .subpage-table th:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
    .bill-subpage .subpage-table th:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
    .bill-subpage .subpage-table th:first-child, .bill-subpage .subpage-table td:first-child { padding-left: var(--space-3); }
    .bill-subpage .subpage-table th:last-child, .bill-subpage .subpage-table td:last-child { padding-right: var(--space-3); }
    .bill-subpage .subpage-table td { padding-top: var(--space-3); padding-bottom: var(--space-3); }
    .bill-subpage .subpage-table tbody tr:last-child td { border-bottom: 0; }
    @media (hover: hover) {
      .bill-subpage .subpage-table tbody tr:hover { background: none; }
      .bill-subpage .subpage-table tbody tr:hover td { background: var(--paper-50); }
    }
    .archived-page .subpage-table th.col-bill { min-width: 14rem; }
    .archived-page .subpage-table td.col-actions { text-align: right; white-space: nowrap; }
  }
  /* Phone: one card per row, each value under its column's name. */
  @media (max-width: 900px) {
    .bill-subpage .subpage-table, .bill-subpage .subpage-table tbody,
    .bill-subpage .subpage-table tr, .bill-subpage .subpage-table td { display: block; width: auto; }
    .bill-subpage .subpage-table thead { display: none; }
    .bill-subpage .subpage-table tr { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--rule); }
    .bill-subpage .subpage-table tr:last-child { border-bottom: 0; }
    .bill-subpage .panel .subpage-table td { border: 0; padding: var(--space-1) 0; }
    .bill-subpage .subpage-table td:empty { display: none; }
    .bill-subpage .subpage-table td[data-label]::before {
      content: attr(data-label); display: block; margin-bottom: var(--space-0);
      font-size: var(--text-xs); font-weight: 500; color: var(--muted);
    }
    .bill-subpage .subpage-table td.lead { font-weight: 500; }
    .bill-subpage .subpage-table td.date { white-space: normal; }
    .bill-subpage .subpage-table .position-badge { white-space: normal; text-align: left; }
    .archived-page .subpage-table td.col-actions { padding-top: var(--space-2); }
    .whip-page .subpage-table td select, .whip-page .subpage-table td input.note { width: 100%; }
  }

  /* Archived: the bill number mono navy over its title. */
  .archived-page .bill-ident { font-weight: 400; color: var(--link); }
  .archived-page .bill-name { font-size: var(--text-sm); font-weight: 500; color: var(--ink); }
  .archived-page #list > .empty { margin: 0; padding: 0 var(--space-4) var(--space-4); }

  /* Sponsors & votes: each bill a row, number before title. */
  .sponsors-page .sponsor-bills { padding: 0 var(--space-2) var(--space-2); }
  .sponsors-page .bill-row.sponsor-bill { flex-direction: column; align-items: stretch; gap: var(--space-2); padding: var(--space-3); }
  .sponsors-page .sponsor-bill + .sponsor-bill { border-top: 1px solid var(--rule); }
  .sponsors-page .bill-row .bill-id { font-size: var(--text-sm); color: var(--ink); margin: 0 0 var(--space-1); }
  .sponsors-page .bill-row .bill-id a { font-size: var(--text-xs); color: var(--link); margin-right: var(--space-1); }
  @media (hover: hover) { .sponsors-page .bill-row .bill-id a:hover { color: var(--ink); text-decoration: underline; } }
  .sponsors-page .sponsor-bill-title { font-weight: 500; }
  .sponsors-page .sponsor-bill .status-badge { margin-bottom: 0; }
  .sponsors-page .member-row { justify-content: space-between; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-3); }
  .sponsors-page .member-row + .member-row { border-top: 1px solid var(--rule); }
  .sponsors-page .member-counts { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: flex-start; }
  .sponsors-page .member-count > summary { cursor: pointer; font-size: var(--text-sm); }
  .sponsors-page .sponsor-positions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); }
  /* Chips: borderless neutral, the words as LegiScan wrote them. */
  .sponsors-page .tag { text-transform: none; letter-spacing: 0; background: var(--paper-250); color: var(--ink-2); }
  /* VerdictChip: its edge a softer step of its own tone, so it reads
     apart from the position badge it judges. The shape stays the one
     pill (the base rule owns radius, border style and weight). */
  .sponsors-page .verdict-chip.with_us, .sponsors-page .verdict-chip.against_us { border-color: color-mix(in srgb, currentColor 45%, transparent); }
  /* VoteBar: venue and motion over the bar, counts in words under it. */
  .sponsors-page .vote-where { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--space-2); }
  .sponsors-page .vote-venue { font-size: var(--text-sm); font-weight: 500; }
  .sponsors-page .vote-when { font-size: var(--text-xs); color: var(--ink-2); }
  .sponsors-page .vote-bar { margin-top: var(--space-2); }
  @media (max-width: 640px) {
    .sponsors-page .sponsor-positions .position-badge { white-space: normal; text-align: left; }
  }

  /* Whip count. */
  .whip-page .whip-seats { margin-top: var(--space-4); }
  .whip-page .whip-tally p { margin: 0 0 var(--space-3); font-size: var(--text-sm); }
  .whip-page .whip-counts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); color: var(--ink-2); }

  /* Import a bill list. */
  .bill-import-page textarea { display: block; margin-bottom: var(--space-3); }
  /* The file picker wraps under its words on a phone, never past the card. */
  .bill-import-page input[type="file"] { max-width: 100%; }
  .bill-import-page .bill-import-notes { padding-bottom: var(--space-3); }
  .bill-import-page .bill-import-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); padding-top: var(--space-3); }
  .bill-import-page .bill-import-done { margin: var(--space-3) 0 0; padding: 0 var(--space-3); font-weight: 500; }

  /* New session: each bill a row with a navy Checkbox. */
  .rollover-page .rollover-list, .rollover-page .rollover-matches { list-style: none; margin: 0 0 var(--space-3); padding: 0; }
  .rollover-page .rollover-row {
    display: flex; align-items: flex-start; gap: var(--space-3);
    padding: var(--space-2); border-radius: var(--radius-md); cursor: pointer;
  }
  @media (hover: hover) { .rollover-page .rollover-row:hover { background: var(--paper-50); } }
  .rollover-page .rollover-row input[type="checkbox"] { flex: none; margin: var(--space-0) 0 0; accent-color: var(--link); }
  .rollover-page .rollover-row-main { display: flex; flex-direction: column; gap: var(--space-0); min-width: 0; }
  .rollover-page .rollover-row-title { font-size: var(--text-sm); font-weight: 500; color: var(--ink); }
  .rollover-page .rollover-row-meta { font-size: var(--text-xs); color: var(--ink-2); }
  .rollover-page .rollover-year { color: var(--muted); font-weight: 400; }
  .rollover-page .rollover-matches { margin-bottom: 0; }
  .rollover-page .match { padding: var(--space-3) 0; }
  .rollover-page .match + .match { border-top: 1px solid var(--rule); }
  .rollover-page .match .sub { margin: var(--space-1) 0 0; font-size: var(--text-xs); }
  .rollover-page .rollover-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
  .rollover-page .rollover-actions:not(:has(button)) { display: none; }
  .rollover-page .offer:not(:empty) { margin-top: var(--space-3); }
  .rollover-page .offer p { margin: 0 0 var(--space-2); font-size: var(--text-sm); }
  .rollover-page #matches > .empty { margin: 0; }

  @media (max-width: 640px) {
    .bill-subpage .page-head h1 { font-size: var(--text-xl); }
  }

  /* Why this row is here. LegiScan matches on full bill text but returns
     none of it, so a title can share no word with the query (live, that
     is ~79% of a broad result set). Where a query term IS in the title
     it gets marked; where it is not, the row says the match was in the
     text rather than leaving the user to guess — see matchNoteHtml(). */
  mark.match-hit {
    background: var(--warn-soft); color: inherit;
    border-radius: var(--radius-sm); padding: 0 var(--space-0);
  }
  .match-note { margin-top: var(--space-1); font-size: var(--text-2xs); color: var(--slate); font-style: italic; }

  /* A real full-text snippet, unlike the italic note above it replaces:
     this is the bill's own words, so it is set upright and readable
     rather than as an aside about the result. Clamped to three lines —
     the point is to see the hit in context, not to read the statute in
     a table cell. */
  .match-snippet {
    font-style: normal; color: var(--ink); line-height: 1.45;
    font-size: var(--text-2xs); max-width: 62ch;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden;
  }

  /* The citations that put a bill in a code-section search. Set as
     chips rather than prose because they are read by scanning — "does
     this one amend or repeal my section" is the question, and the verb
     is what the eye needs to land on first. */
  .match-citations { font-style: normal; display: flex; flex-wrap: wrap; gap: var(--space-1); }
  .cite {
    display: inline-flex; align-items: baseline; gap: var(--space-1);
    border: 1px solid var(--rule); border-radius: var(--radius-sm);
    padding: var(--space-0) var(--space-1); font-size: var(--text-2xs); color: var(--slate);
  }
  .cite strong { color: var(--ink); font-weight: 600; }
  /* Repeal reads as the consequential one, so it carries the warn tone;
     add and amend stay quiet. Colour is not the only signal — the verb
     is written out in each chip — so this stays readable to anyone who
     can't tell the three apart. */
  .cite-repeal { border-color: var(--warn); background: var(--warn-soft); }
  .cite-range {
    font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.03em;
    border: 1px solid var(--rule); border-radius: var(--radius-sm);
    padding: 0 var(--space-1); cursor: help;
  }

  /* Quieter than .result-truncated: that one is a warning about
     incomplete results, this one just restates how a query was read. */
  .result-note {
    background: var(--accent-soft); color: var(--ink); border-radius: var(--radius-md);
    padding: var(--space-2) var(--space-3); margin-bottom: var(--space-3); font-size: var(--text-xs);
  }

  /* Title matches, then text-only matches (lookup's matchGroups). The
     heading names what every row under it has in common, so the cards
     don't each repeat it. */
  .result-group-head {
    font-size: var(--text-sm); font-weight: 600; color: var(--ink);
    margin: var(--space-4) 0 var(--space-2);
  }
  .result-group-head:first-child { margin-top: 0; }
  .result-group-head.is-text-only {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    border-top: 1px solid var(--rule); padding-top: var(--space-4);
  }
  .result-group-head.is-text-only h3 { font-size: var(--text-sm); margin: 0; }
  .result-group-head.is-text-only .sub { margin: var(--space-1) 0 0; font-weight: 400; }
  .result-group-head.is-text-only > button { flex-shrink: 0; }

  /* ── Capitol directory (see directory.py) ────────────────────────
     One flat table, a row per staffer, the office carried in its own
     column. Was a card per office; the table lets a search that spans
     offices read as one list and lets the committee select narrow it
     without collapsing whole cards. The row-level styles (staffer link,
     legislator meta, stale/matched treatment, filter toolbar) live near
     the bottom of this section, beside .dir-search. */
  /* ── Redline (see bill_diff.py) ──────────────────────────────────
     Struck text and its replacement inline, the way a redline reads on
     paper. <del>/<ins> carry the meaning to a screen reader and survive
     a copy-paste, so colour is reinforcement rather than the signal. */
  .redline-picker {
    display: flex; align-items: flex-end; gap: var(--space-2); flex-wrap: wrap;
    padding-bottom: var(--space-3); border-bottom: 1px solid var(--rule); margin-bottom: var(--space-2);
  }
  .redline-picker label { display: flex; flex-direction: column; gap: var(--space-1); }
  /* Bill text is long; the panel scrolls rather than the page. */
  .redline { max-height: 34rem; overflow-y: auto; font-size: var(--text-xs); line-height: 1.55; }
  .redline-block { margin: var(--space-2) 0; }
  .redline-equal { color: var(--slate); }
  .redline del {
    background: var(--warn-soft); color: var(--ink);
    text-decoration: line-through; text-decoration-thickness: 1px;
  }
  .redline ins {
    background: var(--accent-soft); color: var(--ink); text-decoration: none;
    box-shadow: inset 0 -1px 0 var(--accent);
  }
  /* The bill's untouched bulk, said as a count. The point of opening a
     redline is the parts that are not the same. */
  .redline-skip {
    color: var(--slate); font-size: var(--text-2xs); font-style: italic;
    padding: var(--space-1) 0; border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule); margin: var(--space-2) 0;
  }

  /* ── Bill report: tabs, progress, authors, votes (see report_body.html) ──
     The report is a header + progress tracker + a six-tab body. The
     tablist reuses .settings-tabs; the rules below are the parts unique
     to this screen. Colours are the same semantic tokens the rest of the
     app uses (--good/--error/--info with their -soft grounds), so nothing
     new needs the contrast test — the pairs are already covered. */
  .report-tabs { margin-top: var(--space-4); }
  /* Panels stack with a gap inside every tab and column; the first block
     of each stacking context sits flush under the thing above it. */
  .report-panel .panel,
  .report-panel .current-status-card { margin-top: var(--space-4); }
  /* The description sits in the header on a wide screen and under What's
     next on a phone (summaryPanelHtml): at 375px a long one pushed the
     next hearing ~720px down. Same text, two slots, one shown. */
  .bill-desc-phone { display: none; }
  @media (max-width: 640px) {
    .bill-desc-header { display: none; }
    .report-panel .bill-desc-phone { display: block; margin-top: var(--space-4); }
  }
  /* The header's at-a-glance strip (headerCardHtml, audit S6): what's
     next and every client's position, straight under the bill's name.
     Side by side on a wide screen; stacked on a phone, What's next
     first, so the answer is above the fold at 375px. The chips are
     links to the client, read-only — the control is in the Summary. */
  .bill-glance {
    display: flex; flex-wrap: wrap; align-items: flex-start;
    gap: var(--space-3) var(--space-6); margin-top: var(--space-3);
    padding-top: var(--space-3); border-top: 1px solid var(--rule);
  }
  .bill-glance:empty { display: none; }
  .glance-next, .glance-clients { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
  .glance-label { font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--slate); }
  .glance-next-when { font-size: var(--text-sm); font-weight: 600; }
  .glance-next-when.now { color: var(--error); }
  .glance-next-when.soon { color: var(--warn); }
  .glance-next .next-chip { margin-top: 0; align-self: flex-start; }
  .glance-next-where { font-size: var(--text-xs); color: var(--slate); overflow-wrap: anywhere; }
  /* What the latest amendment did to the contents, and the 72-hour note (Oct 7 audit A2). */
  .glance-contents { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-3); }
  .glance-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; margin: 0; padding: 0; }
  .glance-chip { min-height: 1.5rem; text-decoration: none; }
  @media (hover: hover) { .glance-chip:hover { text-decoration: underline; } }
  @media (max-width: 640px) {
    .bill-glance { flex-direction: column; gap: var(--space-3); }
  }
  .bill-header .card-actions { margin-top: var(--space-3); }
  .bill-header .bill-desc-header { margin-top: var(--space-3); }
  /* "Who handles this" inside What's next (renderWhoHandles). */
  .whats-next-who { margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--rule); }
  /* "New since you last opened" (whatsNewStripHtml) — the top of the
     Summary. Its edge is --info, the unread dot's own colour: it is the
     dot's changes, named. Full width above the two columns, one change
     per line so a long hearing line wraps rather than widening a phone. */
  .whats-new-strip {
    border: 1px solid var(--rule-card); border-left: 2px solid var(--info);
    border-radius: var(--radius-lg); padding: var(--space-3) var(--space-5);
    margin-bottom: var(--space-4); background: var(--surface); min-width: 0;
  }
  .whats-new-strip .whats-new-title {
    font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--slate); margin: 0 0 var(--space-2);
  }
  .whats-new-list { list-style: none; margin: 0; padding: 0; }
  .whats-new-list li { overflow-wrap: anywhere; }
  .whats-new-strip .whats-new-more {
    display: inline-flex; align-items: center; min-height: 1.5rem;
    margin-top: var(--space-1); font-size: var(--text-xs);
  }
  /* "Lobbying on this bill" (lobbyingPanelHtml) — the organization pages'
     .lobbying-table, one tbody per quarter led by its quarter's name. */
  .lobbying-caveat { max-width: 44rem; }
  .lobbying-pad { padding: var(--space-3) var(--space-4); margin: 0; }
  .lobbying-totals { padding-bottom: 0; }
  .lobbying-table tr.quarter-row th {
    text-align: left; font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.04em;
    text-transform: uppercase; color: var(--slate); padding: var(--space-3) var(--space-4) var(--space-1);
  }
  .lobbying-earlier .table-scroll { margin: var(--space-2) calc(-1 * var(--space-4)) var(--space-2); }
  @media (max-width: 900px) {
    .lobbying-table tr.quarter-row { padding-bottom: 0; border-bottom: 0; }
    .lobbying-table tr.quarter-row th { display: block; padding: var(--space-3) 0 0; }
  }
  /* "Support & opposition on file" (onFilePanelHtml) — the analysis
     it's as of with its picker, then Support and Opposition side by side,
     stacked on a phone. Names wrap anywhere: an analysis can list a
     coalition's full name as one long unbroken line. */
  .onfile-asof { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); padding-bottom: 0; }
  .onfile-asof > p { margin: 0; }
  .onfile-picker { display: flex; align-items: center; gap: var(--space-2); min-width: 0; max-width: 100%; }
  .onfile-picker select { min-width: 0; max-width: 100%; }
  .onfile-newer { padding-bottom: 0; }
  .onfile-sides {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-4); padding: var(--space-3) var(--space-4);
  }
  @media (max-width: 600px) { .onfile-sides { grid-template-columns: minmax(0, 1fr); } }
  .onfile-side-head { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-2); font-size: var(--text-sm); }
  .onfile-side-head .count { font-weight: 600; color: var(--slate); }
  .onfile-list { list-style: none; margin: 0; padding: 0; }
  .onfile-list li { padding: var(--space-1) 0; border-top: 1px solid var(--rule); overflow-wrap: anywhere; }
  .onfile-list li:first-child { border-top: 0; }
  .onfile-none { margin: 0; }
  .onfile-neutral { overflow-wrap: anywhere; padding-top: 0; }
  .onfile-client {
    display: inline-flex; align-items: center; margin-left: var(--space-1);
    padding: var(--space-0) var(--space-2); border-radius: var(--radius-pill);
    background: var(--accent-soft); color: var(--accent);
    font-size: var(--text-xs); font-weight: 600; line-height: 1.4; white-space: nowrap; vertical-align: middle;
  }
  .report-panel > :first-child,
  .summary-main > :first-child,
  .summary-side > :first-child { margin-top: 0; }

  /* Summary = a wide working column + a narrow author sidebar. */
  .summary-grid {
    display: grid; grid-template-columns: minmax(0, 1fr) 18rem;
    gap: var(--space-4); align-items: start;
  }
  /* minmax(0, 1fr), not 1fr: a bare 1fr track's minimum is its content's
     min-content width, so one long unbroken line (a code citation, a
     wide table) stretched the single column past a 375px screen and every
     panel below the top card ran off the right edge. */
  @media (max-width: 860px) { .summary-grid { grid-template-columns: minmax(0, 1fr); } }

  /* The 8-step legislative progress tracker: dots on a connecting line,
     each dot's incoming segment filled once the bill has passed it. The
     current step is a hollow dot with a gold focus-style ring. */
  .bill-progress {
    list-style: none; display: flex; margin: var(--space-4) 0 0; padding: 0;
    overflow-x: auto; position: relative;   /* offsetParent for scrollTrackerToCurrent() */
    scroll-snap-type: x proximity;   /* a swipe settles on a step's edge, never mid-label */
    container-type: inline-size;     /* the steps size off the track, not the viewport */
  }
  .bp-step {
    flex: 1 0 4.5rem; position: relative; text-align: center; scroll-snap-align: start;
    padding-top: var(--space-5); font-size: var(--text-2xs); color: var(--slate);
  }
  .bp-step::before {
    content: ""; position: absolute; top: 0.58rem; left: -50%; width: 100%;
    height: 2px; background: var(--rule); z-index: 0;
  }
  .bp-step:first-child::before { display: none; }
  .bp-dot {
    position: absolute; top: 0.15rem; left: 50%; transform: translateX(-50%);
    width: 0.85rem; height: 0.85rem; border-radius: var(--radius-pill);
    background: var(--surface); border: 2px solid var(--rule-strong); z-index: 1;
  }
  .bp-step.done::before,
  .bp-step.current::before { background: var(--ink); }
  .bp-step.done .bp-dot { background: var(--ink); border-color: var(--ink); }
  .bp-step.current .bp-dot {
    background: var(--surface); border-color: var(--ink);
    box-shadow: 0 0 0 3px var(--accent-soft);
  }
  .bp-step.done .bp-label,
  .bp-step.current .bp-label { color: var(--ink); }
  .bp-step.current .bp-label { font-weight: 700; }
  /* A bill that stopped short (vetoed, died) ends on its own marker — a
     ✕ in the dot and the way it ended — not on a borrowed "Signed". */
  .bp-step.ended::before { background: var(--ink); }
  .bp-step.ended .bp-dot {
    display: flex; align-items: center; justify-content: center;
    width: 1rem; height: 1rem; top: 0.08rem;
    background: var(--error-soft); border-color: var(--error); color: var(--error);
    font-size: var(--text-2xs); font-weight: 700; line-height: 1;
  }
  .bp-step.ended .bp-label { color: var(--error); font-weight: 700; }
  /* Too narrow for all eight: five steps to a screen, exactly, so every
     scroll stop — the far end included — is a step's edge and no label
     is cut in half (audit Y25). 36rem = eight 4.5rem steps. */
  @container (max-width: 36rem) {
    .bp-step { flex: 1 0 20%; }
  }

  /* Author / coauthor sidebar. Party pills reuse the soft/solid semantic
     pairs so they read the same as every other status chip. */
  .author-name { font-weight: 600; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
  .author-primary .sub { margin: var(--space-0) 0 0; }
  .author-co { margin-top: var(--space-3); }
  .author-co-head { margin: 0 0 var(--space-1); }
  .author-co-list { list-style: none; margin: 0; padding: 0; }
  .author-co-list li { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) 0; font-size: var(--text-sm); }
  .author-asof { margin-top: var(--space-3); font-size: var(--text-2xs); font-style: italic; }

  /* Votes needed (votesNeededPanelHtml; votes_needed.py): the threshold,
     then the remaining steps. Each step's state is a word (.vn-state);
     the left rule only echoes it. */
  .votes-needed-panel { margin-top: var(--space-4); }
  .vn-threshold { font-size: var(--text-sm); }
  .vn-note { margin: var(--space-1) 0 0; font-size: var(--text-2xs); }
  .vn-head { margin: var(--space-3) 0 var(--space-1); }
  .vn-steps { list-style: none; margin: 0; padding: 0; }
  .vn-step { padding: var(--space-2) 0 var(--space-2) var(--space-3); border-left: 2px solid var(--rule); font-size: var(--text-sm); }
  .vn-step.current { border-left-color: var(--accent); }
  .vn-step.done .vn-label, .vn-step.skipped .vn-label { color: var(--slate); }
  .vn-step.stopped { border-left-color: var(--error); }
  .vn-step-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); }
  .vn-label { font-weight: 600; }
  .vn-state { font-size: var(--text-2xs); color: var(--slate); white-space: nowrap; }
  .vn-step.current .vn-state { color: var(--ink); font-weight: 600; }
  .vn-vote { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); margin-top: var(--space-1); }
  .party-pill { background: var(--accent-soft); color: var(--slate); }
  .party-pill.dem { background: var(--info-soft); color: var(--info); }
  .party-pill.rep { background: var(--error-soft); color: var(--error); }
  .party-pill.ind { background: var(--accent-soft); color: var(--slate); }

  /* Votes as proportional aye/nay bars. The bar is a flex row of coloured
     segments; the legend repeats the counts so the numbers aren't carried
     by colour alone. */
  .vote-card { padding: var(--space-3) 0; border-bottom: 1px solid var(--rule); }
  .vote-card:first-child { padding-top: var(--space-1); }
  .vote-card:last-child { border-bottom: 0; padding-bottom: var(--space-1); }
  .vote-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-4); }
  .vote-desc { font-weight: 600; font-size: var(--text-sm); }
  .vote-outcome { }
  .vote-outcome.pass { background: var(--good-soft); color: var(--good); }
  .vote-outcome.fail { background: var(--error-soft); color: var(--error); }
  .vote-meta { margin: var(--space-0) 0 var(--space-2); }
  .vote-bar {
    display: flex; gap: var(--space-0); height: 0.7rem; border-radius: var(--radius-pill);
    overflow: hidden; background: var(--accent-soft);
  }
  .vote-seg { height: 100%; }
  .vote-seg.yea { background: var(--good); }
  .vote-seg.nay { background: var(--error); }
  .vote-seg.other { background: var(--rule-strong); }
  .vote-legend { display: flex; gap: var(--space-4); flex-wrap: wrap; margin-top: var(--space-2); font-size: var(--text-xs); color: var(--slate); }
  .vote-key { display: inline-flex; align-items: center; gap: var(--space-1); }
  .vote-key::before { content: ""; width: 0.6rem; height: 0.6rem; border-radius: var(--radius-xs); background: currentColor; }
  .vote-key.yea::before { background: var(--good); }
  .vote-key.nay::before { background: var(--error); }
  .vote-key.other::before { background: var(--rule-strong); }
  .vote-who-btn { margin-top: var(--space-2); }
  .vote-who { margin-top: var(--space-3); display: grid; gap: var(--space-3); }
  .vote-who[hidden] { display: none; }   /* the display above would otherwise beat [hidden] */
  .vote-who-group h3 { margin: 0 0 var(--space-1); font-size: var(--text-xs); font-weight: 600; color: var(--slate); }
  .vote-who-list { list-style: none; margin: 0; padding: 0; columns: 15rem auto; column-gap: var(--space-4); font-size: var(--text-sm); }
  .vote-who-list li { padding: var(--space-0) 0; break-inside: avoid; }
  .vote-who-list li > * + * { margin-left: var(--space-1); }

  /* The indexed full text (Text tab) — its own scroller, like the redline. */
  .bill-text { max-height: 40rem; overflow-y: auto; font-size: var(--text-sm); line-height: 1.6; }
  .bill-text p { margin: 0 0 var(--space-3); }

  /* Compare, two-pane: facing "Was" / "Now" columns. Each pane keeps the
     existing .redline scroller; a gap block holds the line where one side
     added or removed a paragraph the other doesn't have. */
  .redline-2pane { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
  .redline-pane { min-width: 0; }
  .redline-pane-head {
    margin: 0 0 var(--space-1); font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.04em; font-size: var(--text-2xs);
  }
  .redline-gap { min-height: 1.3rem; border-left: 2px solid var(--rule); opacity: 0.4; margin: var(--space-2) 0; }
  @media (max-width: 720px) { .redline-2pane { grid-template-columns: 1fr; } }

  /* Compare, by section (Oct 7 audit D3): an Inline / Side by side
     toggle, an outline of the changed sections, then each section with
     its own "Copy this change". The sections scroll as one — inside a
     section the panes no longer scroll on their own, so the Was and Now
     of one paragraph stay level. A section a client watches is marked. */
  .redline-toolbar { display: flex; justify-content: flex-end; margin: var(--space-2) 0; }
  .redline-view { display: inline-flex; gap: var(--space-1); }
  .redline-view button[aria-pressed="true"] { color: var(--accent); background: var(--accent-soft); border-color: transparent; }
  .redline-outline ol { margin: 0 0 var(--space-3); padding-left: var(--space-5); font-size: var(--text-xs); }
  .redline-outline li { margin: var(--space-1) 0; }
  .redline-outline li .sub { margin-left: var(--space-2); }
  .redline-outline-client a { font-weight: 600; }
  .redline-sections { max-height: 40rem; overflow-y: auto; }
  .redline-sections .redline { max-height: none; overflow: visible; }
  .redline-section { border-top: 1px solid var(--rule); padding: var(--space-3) 0; }
  .redline-section--client { border-left: 2px solid var(--accent); padding-left: var(--space-3); }
  .redline-section-head {
    display: flex; justify-content: space-between; align-items: flex-start;
    gap: var(--space-3); flex-wrap: wrap; font-size: var(--text-sm);
  }
  .redline-client-note { font-size: var(--text-2xs); color: var(--accent); font-weight: 600; margin-top: var(--space-1); }

  /* ── Bill report v5 (docs/design-system; ui_kits/app BillReport) ──
     Laid over the rules above and over the shared .card / .panel /
     .settings-tabs / .current-status-card ones, scoped to #report (this
     page's own container) and #back-link, so no other page moves. White
     cards on a hairline, no shadow, a serif title row and no header rule;
     the bill number in mono navy ahead of a serif page title; labels in
     sentence case; the progress tracker in its own card, navy as it
     moves. Buttons and fields keep the app's shapes, as on the
     dashboard; in-card actions are the dashboard's 30px .small. */
  #back-link {
    display: inline-flex; align-items: center; gap: var(--space-1); min-height: 1.875rem;
    margin: 0; padding: 0 var(--space-3); border: 1px solid var(--rule-card); border-radius: var(--radius-button);
    background: var(--surface); color: var(--ink-2); font-size: var(--text-xs); font-weight: 500; white-space: nowrap;
  }
  @media (hover: hover) { #back-link:hover { color: var(--ink); border-color: var(--rule-strong); } }

  /* The header card: status line, the title, what's next + clients,
     then the actions under a hairline and the description in a tint. */
  #report .bill-header {
    display: flex; flex-direction: column; gap: var(--space-3);
    border: 1px solid var(--rule); box-shadow: none; padding: var(--space-5);
  }
  #report .bill-header > * { margin: 0; }
  #report .bill-header .status-line { align-items: center; gap: var(--space-1) var(--space-3); }
  #report .bill-header .status-badge { margin: 0; }
  #report .bill-header .status-since { margin: 0; font-size: var(--text-xs); color: var(--muted); }
  #report .bill-title {
    font-family: var(--font-display); font-size: var(--text-display); font-weight: 500;
    line-height: 1.1; letter-spacing: -0.015em; text-wrap: balance;
  }
  #report .bill-title-number {
    font-family: var(--mono); font-size: var(--text-xl); font-weight: 500; letter-spacing: 0; color: var(--link);
  }
  #report .bill-glance { padding-top: 0; border-top: 0; }
  #report .glance-label { font-size: var(--text-2xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted); }
  #report .bill-header .card-actions { padding-top: var(--space-3); border-top: 1px solid var(--rule); }
  #report .card-actions .flagged-mark { color: var(--muted); }
  /* Phone: Draft letter, Copy update and the menu are 13px wider than the
     row, which dropped the menu to a line of its own. */
  @media (max-width: 480px) {
    #report .bill-header .card-actions > button { padding-inline: var(--space-3); }
  }
  #report .bill-desc {
    padding: var(--space-3); border-radius: var(--radius-md); background: var(--paper-50);
    color: var(--ink-2); font-size: var(--text-sm); line-height: 1.55; max-width: 54rem; text-wrap: pretty;
  }
  #report .bill-header .bill-link { align-self: flex-start; font-size: var(--text-xs); color: var(--link); }
  @media (hover: hover) { #report .bill-header .bill-link:hover { color: var(--ink); } }

  /* The progress tracker in its own card; navy where the bill has been. */
  #report .bill-progress-card {
    margin-bottom: var(--space-4); padding: var(--space-4) var(--space-4) var(--space-3);
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius-lg);
  }
  /* The bottom pad keeps a phone's sideways scrollbar off the labels. */
  #report .bill-progress-card .bill-progress { margin: 0; padding-bottom: var(--space-1); }
  #report .bp-step.done::before, #report .bp-step.current::before, #report .bp-step.ended::before { background: var(--link); }
  #report .bp-step.done .bp-dot { background: var(--link); border-color: var(--link); }
  #report .bp-step.current .bp-dot { border-color: var(--link); box-shadow: 0 0 0 3px var(--info-soft); }
  #report .bp-step.done .bp-label { color: var(--ink-2); }
  #report .bp-step.current .bp-label { color: var(--ink); font-weight: 600; }
  #report .bp-step.ended .bp-label { font-weight: 600; }

  /* Tabs: the design system's underline strip — muted words, the
     current one in ink. */
  #report .report-tabs { gap: var(--space-5); margin-top: 0; }
  #report .report-tabs .settings-tab { font-size: var(--text-sm); font-weight: 400; color: var(--muted); }
  #report .report-tabs .settings-tab[aria-selected="true"] { color: var(--ink); font-weight: 500; }
  @media (hover: hover) { #report .report-tabs .settings-tab:hover { color: var(--ink); } }

  /* "New since you last opened": a navy-tinted card, its title led by
     the unread dot it names. */
  #report .whats-new-strip {
    border: 1px solid var(--rule); border-radius: var(--radius-lg); padding: var(--space-4);
    background: color-mix(in srgb, var(--link) 6%, var(--surface));
  }
  #report .whats-new-strip .whats-new-title {
    display: flex; align-items: center; gap: var(--space-2);
    font-size: var(--text-xs); font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--link);
  }
  #report .whats-new-title::before { content: ""; width: 7px; height: 7px; border-radius: var(--radius-pill); background: currentColor; flex: none; }

  /* Cards: hairline edge, no shadow, serif title, no rule under it. */
  #report .panel { border: 1px solid var(--rule); box-shadow: none; }
  #report .panel-head { border-bottom: 0; padding: var(--space-4) var(--space-4) var(--space-1); }
  #report .panel-head .title,
  #report .current-status-card > .current-status-label {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500;
    line-height: 1.2; letter-spacing: -0.01em; text-transform: none; color: var(--ink);
  }
  #report .panel-head .sub { color: var(--muted); }
  /* A card header's action is a 30px small button (the dashboard's
     .small), including the ones shared scripts draw (Log a meeting). */
  #report .panel-head > button.secondary {
    min-height: 1.875rem; padding: 0 var(--space-3); font-size: var(--text-xs);
  }
  .report-panel-body { padding: var(--space-2) var(--space-4) var(--space-4); }
  p.report-panel-body { margin: 0; }
  /* What's next and Latest action are cards too. The milestone's left
     edge on Latest action stays (milestoneClass(), as in History). */
  #report .current-status-card {
    padding: var(--space-4); box-shadow: none;
    border-top-color: var(--rule); border-right-color: var(--rule); border-bottom-color: var(--rule);
  }
  #report .current-status-card.whats-next { border-left: 1px solid var(--rule); }
  #report .current-status-card > .current-status-label { margin-bottom: var(--space-2); }
  #report .current-status-card .current-status-action { font-weight: 600; }
  #report .whats-next-who .current-status-label {
    font-size: var(--text-2xs); font-weight: 500; letter-spacing: 0; text-transform: none;
    color: var(--muted); margin-bottom: var(--space-1);
  }
  /* Letters on this bill: rows inset in a tint, as the kit draws them. */
  #report #letters-list .saved-row {
    padding: var(--space-2) var(--space-3); margin-bottom: var(--space-1);
    border-bottom: 0; border-radius: var(--radius-md); background: var(--paper-50);
  }
  #report .author-name { font-weight: 500; }

  /* Tables: a tinted head row in sentence case; rows tint on hover. */
  #report .panel th {
    background: var(--paper-50); font-size: var(--text-2xs); font-weight: 500;
    letter-spacing: 0; text-transform: none; color: var(--muted);
  }
  @media (hover: hover) { #report .panel tbody tr:hover { background: var(--paper-50); } }
  /* History's chamber / hearing-type column: a plain secondary word in
     sentence case, not a navy all-caps label. */
  #report td.chamber { font-size: var(--text-sm); font-weight: 400; text-transform: none; color: var(--ink-2); }
  #report td.chamber::first-letter { text-transform: uppercase; }
  #report td .tag { text-transform: none; letter-spacing: 0; }
  #report .lobbying-table tr.quarter-row th {
    background: transparent; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--ink-2);
  }
  #report .redline-pane-head { font-size: var(--text-xs); letter-spacing: 0; text-transform: none; color: var(--muted); }

  /* Votes: each roll call in its own tinted, rounded block; "Who
     voted" opens as a white card inside it. */
  #report .vote-card,
  #report .vote-card:first-child,
  #report .vote-card:last-child {
    padding: var(--space-3); margin-bottom: var(--space-1);
    border-bottom: 0; border-radius: var(--radius-md); background: var(--paper-50);
  }
  #report .vote-who {
    padding: var(--space-3); border: 1px solid var(--rule); border-radius: var(--radius-md); background: var(--surface);
  }

  @media (max-width: 640px) {
    #report .bill-header { padding: var(--space-4); }
    #report .bill-title { font-size: var(--text-lg); }
    #report .bill-title-number { font-size: var(--text-md); }
  }


  .head-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }

  /* One suggested recipient on the letter editor (see routing.py). A
     row rather than a card: this is a list to scan down, and the reason
     each name is on it matters as much as the name. */
  .routing-row {
    display: flex; justify-content: space-between; align-items: flex-start;
    gap: var(--space-4); padding: var(--space-2) 0; border-bottom: 1px solid var(--rule);
  }
  .routing-row:last-child { border-bottom: 0; }
  .routing-row .sub { margin: var(--space-0) 0 0; font-size: var(--text-xs); }
  .routing-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); flex-shrink: 0; }
  @media (max-width: 640px) {
    .routing-row { flex-direction: column; }
  }
  /* The other house's staff (D3): collapsed under the hearing's own
     house, since they cover the subject but not this committee. */
  .routing-other { margin-top: var(--space-2); border-top: 1px solid var(--rule); padding-top: var(--space-2); }
  .routing-other > summary { cursor: pointer; font-size: var(--text-sm); color: var(--slate); }
  /* Only the top match shows; the rest fold behind a "Show N more"
     button (N3-b), at every width. The button is .linklike; this gives
     it a finger-sized hit area without making it look like a button. */
  .routing-more-toggle {
    display: inline-flex; align-items: center; min-height: 2.75rem;
    padding: var(--space-1) 0; font-size: var(--text-sm);
  }
  .routing-directory-link { margin: var(--space-2) 0 0; font-size: var(--text-sm); }
  /* "Address to" / "Brief and follow up" (Oct 7 audit P4): the two
     halves of the letter editor's panel, labelled like a field label. */
  .routing-heading {
    color: var(--ink); font-weight: 600; font-size: var(--text-xs);
    margin: var(--space-3) 0 var(--space-1);
  }
  .routing-heading:first-child { margin-top: 0; }

  /* The flat table. Cells top-align so a staffer's two lines (name +
     title) and a legislator's two lines (name + meta) read straight
     across, and the committee chips wrap without dragging the row's
     other cells to the middle. */
  .dir-table td { vertical-align: top; }
  .dir-staffer { font-weight: 600; }
  @media (hover: hover) { .dir-staffer:hover { color: var(--gold); } }
  .dir-legislator { font-size: var(--text-sm); }
  /* .sub carries a 2rem bottom margin for use as a page subtitle. In a
     table cell that turns a two-line staffer into a 212px row, so it is
     reset for the secondary lines inside this page's tables. */
  .dir-table .sub { margin: 0; font-size: var(--text-xs); }
  .dir-legislator-meta { display: inline-flex; align-items: center; gap: var(--space-2); }
  /* The party dot — a small square in the party's own hue (see
     partyDotColor() in directory_body.html), so the chamber/district/
     party line carries the party at a glance without a coloured word. */
  .party-dot { width: 7px; height: 7px; border-radius: var(--radius-xs); flex: none; }
  .dir-asof { white-space: nowrap; }
  .dir-asof > div { font-size: var(--text-xs); color: var(--slate); }
  /* The stale pill and its toggle sit under the date, stacked. The pill
     is the warn hue as an outline; the toggle is a plain text control
     (.linklike) so the row isn't crowded with a button per line. */
  .dir-stale-badge { margin-top: var(--space-1); border-color: var(--warn); color: var(--warn); }
  .dir-stale-toggle { display: block; margin-top: var(--space-1); font-size: var(--text-xs); }
  /* The row's own ways in — the staffer's direct line and email under
     their title, the office's room and main line under the office. */
  .dir-reach { margin-top: var(--space-0); overflow-wrap: anywhere; }
  /* An office the sheet lists with nobody under it: still a row, with
     its room and main line, quieter than a named contact. */
  .dir-office-row .dir-staffer { color: var(--slate); }
  /* "Showing 50 of 496" and the Show more button under the table. */
  .dir-more {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
    padding: var(--space-3) 0;
  }
  .dir-more .sub { margin: 0; font-size: var(--text-xs); }

  /* Phone: the same card reflow the bill tables use (.bill-cards, P2-23)
     — the four-column table was 440px of content in a 327px column,
     scrolled sideways. Each row becomes a stacked block under a rule;
     the staffer and office lead, committees follow with a label, and
     the as-of date sits on one line with its stale toggle. */
  @media (max-width: 900px) {
    .dir-table, .dir-table tbody, .dir-table tr, .dir-table td { display: block; width: auto; }
    .dir-table thead { display: none; }
    .dir-table tr { border-bottom: 1px solid var(--rule-strong); padding: var(--space-3) 0; }
    .dir-table tr:last-child { border-bottom: 0; }
    .dir-table td { border: 0; padding: var(--space-1) 0; }
    /* A labelled dash says nothing a card can't say by leaving it out. */
    .dir-table td.col-committees.is-empty { display: none; }
    .dir-table td.col-committees::before {
      content: "Committees"; display: block; margin-bottom: var(--space-1);
      font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.04em;
      text-transform: uppercase; color: var(--slate);
    }
    .dir-table td.col-asof {
      display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
      white-space: normal;
    }
    .dir-table td.col-asof .dir-stale-badge, .dir-table td.col-asof .dir-stale-toggle { margin-top: 0; }
  }
  /* A contact somebody has reported as wrong. Dimmed rather than
     hidden: it is still the only number anyone has, and the person
     dialling it should see both that it exists and that it's suspect.
     Dimmed by --slate, not opacity: 0.55 took the number itself below
     AA, and the Stale badge already says why the row is quieter. */
  .stale-row td { color: var(--slate); }
  .stale-note { color: var(--warn); font-style: italic; }
  /* Which staffer the search actually hit. The office is shown whole
     for context, so without this a search for "water" lists four names
     and says nothing about which one handles water. */
  .matched-row { background: var(--accent-soft); }
  .matched-note { color: var(--accent); font-style: italic; }
  /* A value the firm typed over the sheet (contact_detail_body.html):
     the "Your edit" pill, then the sheet's own value one click away.
     Wraps under the value on a phone rather than pushing it wide. */
  .dir-edit-badge { color: var(--accent); background: var(--accent-soft); }
  .dir-edit-line {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2);
    margin-top: var(--space-1); font-size: var(--text-xs);
  }
  .dir-head-line { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
  .dir-head-line .dir-edit-line { margin-top: 0; }
  .dir-sheet-changed { color: var(--warn); font-weight: 600; }
  .dir-use-sheet { font-size: var(--text-xs); text-align: left; }
  .dir-head-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
  .dir-notes { margin: var(--space-1) 0 0; font-size: var(--text-sm); white-space: pre-wrap; overflow-wrap: anywhere; }
  /* The contact's edit form: one labelled field per row, full width. */
  .dir-edit-form { display: grid; gap: var(--space-3); margin: var(--space-4) 0 0; }
  .dir-edit-field input, .dir-edit-field textarea { width: 100%; }
  .dir-edit-hint { margin: 0; }
  .dir-edit-actions { margin-top: 0; }
  /* Assignment chips reuse .cite from the code-section results — same
     job, a small labelled fact attached to a row. */
  .cite-caucus { border-color: var(--accent); }
  .cite-issue { border-style: dashed; }

  /* The filter toolbar: a search box that grows, then the chamber and
     committee selects. Wraps to stacked full-width controls on a phone.
     The search box is a bordered field wrapping the icon + a borderless
     input, so the icon sits inside the field rather than beside it.
     Same rounded-rectangle shape as .search-bar-field on /lookup. */
  .dir-toolbar { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }
  .dir-toolbar[hidden] { display: none; }   /* the display above would otherwise beat [hidden] */
  .dir-search-box {
    display: flex; align-items: center; gap: var(--space-2); flex: 1 1 16rem;
    min-width: 0; padding: 0 var(--space-3); height: 2.5rem;
    border: 1px solid var(--rule-strong); border-radius: var(--radius-lg);
    background: var(--surface); color: var(--slate);
  }
  .dir-search-box:focus-within { border-color: var(--gold); outline: 2px solid var(--gold); outline-offset: 2px; }
  .dir-search-box svg { flex: none; }
  .dir-search-box input {
    flex: 1; min-width: 0; border: 0; padding: 0; background: transparent; color: var(--ink);
  }
  .dir-toolbar select { flex: 0 0 auto; }
  @media (max-width: 640px) {
    .dir-toolbar select { flex: 1 1 100%; }
  }
  .import-row { display: flex; align-items: flex-end; gap: var(--space-3); flex-wrap: wrap; }
  .import-actions { display: flex; gap: var(--space-2); margin-top: var(--space-4); }
  .file-drop {
    display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer;
    border: 1px dashed var(--rule-strong); border-radius: var(--radius-md);
    padding: var(--space-2) var(--space-4); font-size: var(--text-xs);
  }
  @media (hover: hover) { .file-drop:hover { border-color: var(--accent); color: var(--accent); } }
  /* The mapping table can be ninety rows long (one per column of the
     sheet), so its own scroller is capped in height — .table-scroll
     already handles the sideways case for every table here. */
  #map-table { font-size: var(--text-xs); }
  #import-step-map .table-scroll { max-height: 26rem; overflow-y: auto; }
  /* Under each role select: how that column will actually be read —
     "Committee" means two different things depending on whether the
     header names one committee or just says "Committees". */
  .map-role-note { margin-top: var(--space-1); }
  /* .modal-panel label is display:block and modal inputs stretch, so the
     box-then-words row is spelled out at that specificity. */
  .modal-panel .map-commas { display: flex; align-items: center; gap: var(--space-1);
    margin-top: var(--space-1); font-size: var(--text-xs); }
  .modal-panel .map-commas[hidden] { display: none; }   /* the display above would otherwise beat [hidden] */
  .modal-panel .map-commas input { width: auto; flex: none; margin: 0; }
  /* "Add a contact": the office-or-committee choice, as one row of
     radios borrowing the box-then-words row above. */
  .modal-panel .add-where { border: 0; padding: 0; margin: 0; display: flex;
    flex-wrap: wrap; gap: var(--space-1) var(--space-4); }
  .modal-panel .add-where legend { padding: 0; margin-bottom: var(--space-1); width: 100%; }
  .modal-panel .add-where .map-commas { font-size: var(--text-sm); margin-top: 0; }
  #add-committee-wrap[hidden], #add-office-wrap[hidden] { display: none; }
  /* The dry-run counts, the sheet's warnings, and the stale flags that
     won't carry over — above the Import button, and again on the page
     afterwards, as a short list. */
  .import-result-list { margin: var(--space-1) 0 0; padding-left: var(--space-5); }
  /* The import dialog holds a column-mapping table dozens of rows wide,
     so it opts out of .modal-panel's 26rem cap — wide enough for the
     table, still capped shy of the viewport on a laptop. */
  .modal-panel-wide { max-width: 44rem; }

  /* ── Capitol directory v5 (docs/design-system: Card, data, forms) ──
     Laid over the directory rules above, scoped to .directory-page
     (DIRECTORY_BODY; the contact page keeps its own). Serif page, card
     and dialog titles; the three counts as StatTiles led by a ringed
     IconTile, hairline edge, no lift; the staff table in a flush Card
     whose head is a tinted, rounded band in sentence case, whose rows
     tint on hover, and whose "Showing N of M" is the footer under a
     hairline; committee / caucus / issue chips and the Stale pill as
     borderless tinted pills. Buttons and fields keep the app's own
     shape (button shape is to change app-wide in one pass), as do the
     --gold focus ring, the party dot and the phone card reflow. */
  .directory-page .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em;
  }
  .directory-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }

  /* StatTiles: icon tile beside the number, label and foot under it. */
  .dir-stats .stat-card {
    display: grid; grid-template-columns: 2.75rem minmax(0, 1fr); column-gap: var(--space-3); align-items: center;
    border-color: var(--rule); box-shadow: none; padding: var(--space-4);
  }
  @media (hover: hover) { .dir-stats .stat-card:hover { box-shadow: none; } }
  .dir-stats .icon-tile {
    grid-row: 1 / span 3; width: 2.75rem; height: 2.75rem; box-shadow: inset 0 0 0 1px var(--rule);
  }
  .dir-stats .icon-tile svg { width: 1.125rem; height: 1.125rem; }
  .dir-stats .stat-value { letter-spacing: -0.02em; line-height: 1; }
  .dir-stats .stat-label { margin-top: var(--space-1); font-size: var(--text-sm); color: var(--ink); }
  .dir-stats .stat-foot { margin-top: var(--space-0); font-size: var(--text-2xs); color: var(--muted); }
  @media (min-width: 641px) {
    .dir-stats { grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }
  }
  /* Phone: two tiles a row (as before), the icon tile beside the number
     and the words under both. */
  @media (max-width: 640px) {
    .dir-stats .stat-card { grid-template-columns: auto minmax(0, 1fr); row-gap: 0; padding: var(--space-3); }
    .dir-stats .icon-tile { grid-row: 1; width: 2.25rem; height: 2.25rem; }
    .dir-stats .stat-label, .dir-stats .stat-foot { grid-column: 1 / -1; }
    .dir-stats .stat-label { margin-top: var(--space-2); }
    .directory-page .page-head h1 { font-size: var(--text-xl); }
  }

  /* The staff table's flush Card. */
  .directory-page .dir-card {
    background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius-lg); overflow: hidden;
  }
  .directory-page .dir-card .dir-more { padding: var(--space-3) var(--space-4); border-top: 1px solid var(--rule); }
  .directory-page .dir-more .sub { color: var(--muted); }
  /* A row: the staffer in ink (underlined on hover — a link to their
     page), the office in ink over muted meta. */
  .directory-page a.dir-staffer { font-size: var(--text-sm); font-weight: 500; color: var(--ink); }
  @media (hover: hover) { .directory-page a.dir-staffer:hover { color: var(--ink); text-decoration: underline; } }
  .directory-page .dir-office-row .dir-staffer { font-size: var(--text-sm); font-weight: 500; color: var(--ink-2); }
  .directory-page .dir-legislator { font-weight: 500; color: var(--ink); }
  .directory-page .dir-table .sub { color: var(--ink-2); }
  .directory-page .dir-asof > div { color: var(--muted); }
  /* A stale row stays dimmed, the names included. */
  .directory-page .stale-row :is(a.dir-staffer, .dir-legislator, .sub) { color: var(--slate); }
  /* Chips: borderless tinted pills. Caucus takes the navy tint; an issue
     area keeps its dashed edge, so the three still differ without colour. */
  .directory-page .dir-table .cite {
    align-items: center; min-height: 1.375rem; margin: 0 var(--space-1) var(--space-1) 0; padding: 0 var(--space-2);
    border: 0; border-radius: var(--radius-pill); background: var(--paper-250); color: var(--ink-2);
    font-size: var(--text-2xs); font-weight: 500; line-height: 1.3;
  }
  .directory-page .dir-table .cite-caucus { background: var(--info-soft); color: var(--link); }
  .directory-page .dir-table .cite-issue { border: 1px solid var(--rule-card); border-style: dashed; }
  .directory-page .dir-stale-badge { border-color: transparent; background: var(--warn-soft); color: var(--warn); }
  @media (min-width: 901px) {
    .directory-page .dir-card .table-scroll { padding: var(--space-2) var(--space-2) 0; }
    .directory-page .dir-table { border-collapse: separate; border-spacing: 0; }
    .directory-page .dir-table th {
      border-top: 0; border-bottom: 0; background: var(--paper-50); padding-top: var(--space-2); padding-bottom: var(--space-2);
      font-size: var(--text-2xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
    }
    .directory-page .dir-table th:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
    .directory-page .dir-table th:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
    .directory-page .dir-table th:first-child, .directory-page .dir-table td:first-child { padding-left: var(--space-3); }
    .directory-page .dir-table th:last-child, .directory-page .dir-table td:last-child { padding-right: var(--space-3); }
    .directory-page .dir-table td { padding-top: var(--space-3); padding-bottom: var(--space-3); }
    .directory-page .dir-table tbody tr:last-child td { border-bottom: 0; }
    @media (hover: hover) { .directory-page .dir-table tbody tr:hover td { background: var(--paper-50); } }
  }
  /* The phone card reflow, inside the card: inset from its edge, a
     hairline between people, the Committees label in sentence case. */
  @media (max-width: 900px) {
    .directory-page .dir-card .table-scroll { padding: 0 var(--space-4); }
    .directory-page .dir-table tr { border-bottom-color: var(--rule); }
    .directory-page .dir-table td.col-committees::before {
      font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
    }
  }

  /* Committee rosters: a card with a serif title. */
  .directory-page .card { border-color: var(--rule); box-shadow: none; }
  .directory-page .card > h2.section {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500;
    line-height: 1.2; letter-spacing: -0.01em; margin: 0 0 var(--space-2);
  }
  .directory-page .card .sub { color: var(--ink-2); max-width: 46rem; text-wrap: pretty; }
  .directory-page .field-label { font-weight: 500; }
  /* The dialogs' titles in the card serif. */
  .directory-page .modal-head .title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500;
    line-height: 1.2; letter-spacing: -0.01em;
  }
  .directory-page .modal-head .sub { color: var(--ink-2); }
  /* The column-mapping and pasted-roster check tables: inset on a
     hairline, a tinted sentence-case head. */
  .directory-page :is(#import-step-map, #roster-preview) .table-scroll { border: 1px solid var(--rule); border-radius: var(--radius-md); }
  .directory-page :is(#import-step-map, #roster-preview) th {
    background: var(--paper-50); font-size: var(--text-2xs); font-weight: 500;
    letter-spacing: 0; text-transform: none; color: var(--muted);
  }
  .directory-page :is(#import-step-map, #roster-preview) tbody tr:last-child td { border-bottom: 0; }
  .directory-page #map-body td > strong { font-weight: 500; }

  /* ── Contact page v5 (docs/design-system: Card, data, forms) ──
     One directory contact (CONTACT_DETAIL_BODY), laid over the directory
     rules above and scoped to .contact-page, since .client-facts, .cite,
     .result-truncated and the "Your edit" pill are shared. The name as a
     serif title in a white card on a hairline with no shadow; fact labels
     in sentence case over ink values; committee chips as borderless
     tinted pills; the as-of line and actions as the card's footer under
     a hairline. Buttons and fields keep the app's own shape (that changes
     app-wide in one pass), as do the --gold focus ring and the meeting
     log below the card, which is shared. */
  .contact-page .contact-card {
    display: flex; flex-direction: column; gap: var(--space-4);
    border: 1px solid var(--rule); box-shadow: none; padding: var(--space-5);
  }
  .contact-page .contact-card > * { margin: 0; }
  .contact-page .office-head { display: flex; flex-direction: column; gap: var(--space-3); }
  .contact-page .office-head > div:first-child { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
  .contact-page .office-name {
    margin: 0; font-family: var(--font-display); font-size: var(--text-display); font-weight: 500;
    line-height: 1.1; letter-spacing: -0.015em; text-wrap: balance; overflow-wrap: anywhere;
  }
  .contact-page .contact-edit-title { font-size: var(--text-md); line-height: 1.2; letter-spacing: -0.01em; }
  .contact-page .contact-card .sub { margin: 0; font-size: var(--text-sm); color: var(--ink-2); }
  .contact-page .dir-edit-badge { border-color: transparent; }
  .contact-page .dir-use-sheet { color: var(--link); }
  /* The two banners: a borderless amber tint, the stale one with its
     button on the same line when there is room. */
  .contact-page .contact-banner { margin: 0; font-size: var(--text-sm); }
  .contact-page .contact-banner-stale { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
  /* Facts: muted sentence-case labels over ink values, none bolded. */
  .contact-page .client-facts { margin: 0; padding: 0; border-top: 0; }
  .contact-page .client-fact { min-width: 0; }
  .contact-page .client-fact-label {
    font-size: var(--text-2xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
  }
  .contact-page .client-fact-value { color: var(--ink); overflow-wrap: anywhere; }
  .contact-page .client-fact:first-child .client-fact-value { font-weight: 400; }
  .contact-page .contact-copy { margin-left: var(--space-2); font-size: var(--text-xs); }
  .contact-page .contact-block .client-fact-label { display: block; }
  .contact-page .contact-chips { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-2); }
  .contact-page .contact-chips .cite {
    align-items: center; min-height: 1.375rem; padding: 0 var(--space-2);
    border: 0; border-radius: var(--radius-pill); background: var(--paper-250); color: var(--ink-2);
    font-size: var(--text-2xs); font-weight: 500; line-height: 1.3;
  }
  .contact-page .dir-notes { color: var(--ink); }
  /* The footer: the as-of line, Flag as stale and Back, under a hairline. */
  .contact-page .contact-foot { margin: 0; padding-top: var(--space-3); border-top: 1px solid var(--rule); }
  .contact-page .contact-foot .sub { font-size: var(--text-xs); color: var(--muted); }
  .contact-page .contact-foot-back { margin-left: auto; }
  .contact-page .field-label { font-weight: 500; }
  .contact-page .dir-edit-hint { color: var(--muted); }
  @media (max-width: 640px) {
    .contact-page .contact-card { padding: var(--space-4); }
    .contact-page .office-name { font-size: var(--text-xl); }
    .contact-page .contact-edit-title { font-size: var(--text-md); }
  }

  /* Labels that exist for screen readers only. Was repeated inline on
     every field that needed one; named here since the directory's
     mapping table generates them per column. */
  .visually-hidden {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap;
  }

  /* A button that has to read as a link because it sits mid-sentence.
     Kept a <button> so it is reachable and announced as the action it
     is, rather than an <a href="#"> that goes nowhere. */
  .linklike {
    background: none; border: 0; padding: 0; font: inherit; color: var(--accent);
    text-decoration: underline; cursor: pointer;
  }
  @media (hover: hover) { .linklike:hover { color: var(--accent-solid); } }
  /* On a line of its own — a toolbar, a chip row, a form's Cancel — it
     is a standalone target, and it measured 19px tall ("Select all 25",
     "Show all 1,504"): under WCAG 2.5.8's 24px, the same fix as audit
     F24's links. Opt-in by where it sits, not on .linklike itself: one
     mid-sentence (.result-note, .result-truncated) is exempt as inline
     text, and padding it out would break the line it sits in. */
  .panel-head .linklike, .active-filters .linklike, .save-search-form .linklike, .flag-draft .linklike {
    display: inline-flex; align-items: center; min-height: 1.5rem;
  }
  .panel-head .linklike[hidden] { display: none; }   /* the display above would otherwise beat [hidden] */

  /* Said out loud when filters cover only a prefix of the matches (see
     SEARCH_PAGE_CAP). A filter over part of a result set that presents
     itself as a filter over all of it is the failure this avoids. */
  .result-truncated {
    background: var(--warn-soft); color: var(--ink); border-radius: var(--radius-md);
    padding: var(--space-2) var(--space-3); margin-bottom: var(--space-3); font-size: var(--text-xs);
  }

  /* ── Disclosure review: making sign-off a real review (P1-21) ──
     Three additions, all answering the same thing: sign-off recorded who
     attested, and gave them nothing to attest against. */

  /* What is still outstanding, counted, at the top, with a jump link per
     row. A red asterisk per field says which ones matter; it does not
     say how many are left or where they are, and on a five-page form
     that difference is a hunt. */
  .review-banner {
    background: var(--warn-soft); border: 1px solid var(--rule);
    border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-5);
  }
  .review-banner.ok { background: var(--good-soft); }
  .review-banner-head { font-size: var(--text-sm); font-weight: 700; margin-bottom: var(--space-2); }
  .review-banner.ok { font-size: var(--text-sm); }
  .review-banner-list { margin: 0; padding-left: var(--space-4); display: grid; gap: var(--space-0); }
  .review-banner-list button {
    background: none; border: 0; padding: 0; font: inherit; font-size: var(--text-xs);
    color: var(--ink); text-align: left; cursor: pointer; text-decoration: underline;
    text-underline-offset: 2px;
  }
  @media (hover: hover) { .review-banner-list button:hover { color: var(--accent); } }

  .field-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-1); }
  /* The asterisk carries a title, because a red glyph on its own means
     "required" only to someone who already knows that convention — and
     it is the one cue on this screen that is otherwise colour alone. */
  /* No cursor:help any more — the asterisk carried its own explanation
     in a title= that never appeared on a phone. Every form that uses it
     now states what it means once, in a .req-legend above the fields. */
  .req { color: var(--error); font-weight: 700; }
  .req-legend { font-size: var(--text-xs); margin: 0 0 var(--space-4); }
  /* The signup password hint, once it knows whether it's been met.
     --good/--error rather than colour alone: the ✓ and the reworded
     sentence carry the state on their own. */
  .hint-met { color: var(--good); }
  .hint-unmet { color: var(--error); }
  /* Its checklist form: one row per server rule (accounts.PASSWORD_CHECKS). */
  .pw-rules { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-0); }
  .pw-rules li { display: flex; gap: var(--space-2); align-items: baseline; }
  .pw-rule-mark { width: 1em; flex: none; text-align: center; }
  /* Red only once the field has been touched (or a generate/sign-off was
     tried) — see disclosure_review_body.html's issueRevealed(). The
     message is rendered either way, hidden until then. */
  .field-block.has-issue.issue-shown input,
  .field-block.has-issue.issue-shown textarea { border-color: var(--error); }
  .field-issue { font-size: var(--text-xs); color: var(--error); margin-top: var(--space-1); }
  .field-block.has-issue:not(.issue-shown) .field-issue { display: none; }

  /* Provenance. Every field on this screen looked hand-typed, so a
     reviewer could not tell a value pulled from Profile from one typed
     over it. `edited` is the state worth a second look, so it is the
     one that carries weight — the others are quiet by design, since
     "this is still what Profile says" is the resting state of most of
     the form and should not shout on every field. */
  .provenance { font-size: var(--text-2xs); color: var(--slate); margin-top: var(--space-1); }
  .provenance a { color: inherit; display: inline-flex; align-items: center; min-height: 1.5rem; }
  .provenance.edited { color: var(--ink); }
  .provenance-was {
    font-weight: 600; background: var(--warn-soft);
    border-radius: var(--radius-sm); padding: 0 var(--space-1);
  }
  /* The resting states ("From profile", "Not in profile") as a tag on
     the label's own line rather than a sentence under the input — see
     provenanceTagHtml(). Same 24px target as .provenance a. */
  .field-label-meta { display: inline-flex; align-items: baseline; gap: var(--space-2); }
  .provenance-tag {
    font-size: var(--text-2xs); color: var(--slate); white-space: nowrap;
    display: inline-flex; align-items: center; min-height: 1.5rem;
  }
  .provenance-tag.blank { font-style: italic; }

  /* Fields sized to what they hold (audit F7): a six-column grid, each
     field spanning what disclosure_review_body.html's fieldSpan() gives
     it. On a phone the narrow ones pair up two to a row and the
     half-width ones go full width. */
  .field-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 0 var(--space-3); }
  .field-grid .field-block { display: block; margin-bottom: var(--space-2); min-width: 0; }
  .field-grid .span-1 { grid-column: span 1; }
  .field-grid .span-2 { grid-column: span 2; }
  .field-grid .span-3 { grid-column: span 3; }
  .field-grid .span-4 { grid-column: span 4; }
  .field-grid .span-6 { grid-column: span 6; }
  .field-grid .field-label-row { flex-wrap: wrap; gap: 0 var(--space-2); }
  @media (max-width: 640px) {
    .field-grid .span-1, .field-grid .span-2, .field-grid .span-4 { grid-column: span 3; }
    .field-grid .span-3 { grid-column: span 6; }
  }

  /* Autosave, said out loud. "Edits save automatically as you leave each
     field" was the only evidence anything saved at all, on a screen
     whose output is a document somebody attests to. */
  .field-saved { min-height: 1em; }
  .field-saved.save-status { font-size: var(--text-2xs); }   /* the shared indicator, a size down beside a label */

  /* ── Disclosure review: client rows (P1-20) ──
     Only rows holding something are rendered now, so each row needs its
     own header: who is in it, which row number the PDF will use, and a
     way off the form. The row number stays visible because the paper
     form and the generated PDF both count in them — a reviewer checking
     one against the other is matching row 7 to row 7. */
  .client-row-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-2); margin-bottom: var(--space-2);
  }
  .client-row-meta { display: flex; align-items: center; gap: var(--space-2); flex: none; }
  /* A client card sits inside the Clients card, and on a phone the
     page gutter plus two cards' side padding left each field 227px wide
     (audit N4). The inner card keeps its border and loses most of its
     padding; the outer one trims to match. */
  @media (max-width: 640px) {
    #sec-clients.card { padding-left: var(--space-3); padding-right: var(--space-3); }
    .client-row-card.card { padding: var(--space-3) var(--space-2); }
  }
  .client-row-add {
    display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
    padding-top: var(--space-2);
  }
  /* The clients left off a Form 601 (workflow audit C3): each one named,
     with whether it's ticked and the one button that adds it. */
  .not-on-form { flex-direction: column; align-items: stretch; }
  .not-on-form-list { list-style: none; margin: 0; padding: 0; }
  .not-on-form-item {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    flex-wrap: wrap; padding: var(--space-2) 0; border-top: 1px solid var(--rule);
  }
  .not-on-form-name { min-width: 0; overflow-wrap: anywhere; }

  /* Generate PDF is this page's primary action and used to sit below
     everything it acts on — under nine client cards, roughly thirty of
     whose fields were blank. Pinned to the bottom of the viewport it is
     reachable from anywhere in the form, and it carries the one line
     saying whether the PDF currently matches the page. */
  .sticky-actions {
    position: sticky; bottom: 0; z-index: 5;
    display: flex; align-items: center; justify-content: flex-end; gap: var(--space-3);
    flex-wrap: wrap; margin: 0 0 var(--space-4);
    background: var(--surface); border: 1px solid var(--rule);
    border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
    box-shadow: var(--shadow-up);
  }
  /* A toast on this page ("Sign-off undone", "Also saved to your firm's
     details") sits above that pinned bar, not over its Generate button —
     on a phone it covered the button for the toast's nine seconds. */
  body:has(.sticky-actions) .toast { bottom: calc(var(--space-8) + var(--space-4)); }

  /* ── Disclosure review as a guided flow (audit S5) ──
     The form's sections in a sticky rail beside it, each with its own
     count, and one running total in the sticky bar above — chunking and
     a visible finish line make a long form feel shorter. Same 220px rail
     grammar as .settings-layout. On a phone the rail becomes a strip of
     chips that scrolls sideways within itself, above the form. */
  .guided-layout {
    display: grid; grid-template-columns: 220px minmax(0, 1fr);
    gap: clamp(var(--space-4), 3vw, var(--space-6)); align-items: start;
  }
  .guided-main { min-width: 0; }
  .guided-main > [id^="sec-"] { scroll-margin-top: var(--space-4); }
  .section-nav { position: sticky; top: var(--space-4); }
  .section-nav ol { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-0); }
  .section-nav-item {
    display: flex; flex-direction: column; gap: var(--space-0); padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md); border-left: 2px solid transparent;
    color: var(--slate); text-decoration: none; font-size: var(--text-sm); font-weight: 600;
  }
  @media (hover: hover) { .section-nav-item:hover { color: var(--ink); background: var(--accent-soft); } }
  .section-nav-item[aria-current="location"] { color: var(--ink); background: var(--accent-soft); border-left-color: var(--ink); }
  .section-nav-count { font-size: var(--text-xs); font-weight: 400; color: var(--slate); }
  /* A tick as well as the colour, so "finished" isn't colour alone. */
  .section-nav-item.done .section-nav-count { color: var(--good); }
  .section-nav-item.done .section-nav-count::before { content: "\2713\00a0"; }

  .form-progress { display: flex; flex-direction: column; gap: var(--space-1); margin-right: auto; min-width: 12rem; }
  .form-progress-text { font-size: var(--text-sm); font-weight: 600; }
  /* The phone's shorter copy — shown instead of the sentence at ≤768px. */
  .form-progress-text .fp-short { display: none; }
  .form-progress-track {
    height: 4px; border-radius: var(--radius-pill); overflow: hidden;
    background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--rule);
  }
  .form-progress-fill { display: block; height: 100%; width: 0; background: var(--good); transition: width 0.2s ease; }
  /* Amber, not green, while a section is still empty (unfinishedSections). */
  .form-progress-fill.incomplete { background: var(--warn); }
  @media (prefers-reduced-motion: reduce) { .form-progress-fill { transition: none; } }

  @media (max-width: 768px) {
    .guided-layout { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }
    .section-nav { position: static; min-width: 0; }
    .section-nav ol { flex-direction: row; gap: var(--space-2); overflow-x: auto; padding-bottom: var(--space-1); scrollbar-width: thin; }
    .section-nav-item {
      flex: none; white-space: nowrap; padding: var(--space-1) var(--space-3);
      border: 1px solid var(--rule-strong); border-radius: var(--radius-pill);
    }
    .section-nav-item[aria-current="location"] { border-color: var(--ink); }
    /* One row on a phone — the total and the button. The PDF-state line
       is the "Review & sign off" chip's status already, and a three-row
       bar pinned over the form ate a fifth of the screen. */
    .sticky-actions { flex-wrap: nowrap; }
    .sticky-actions > button { flex: none; white-space: nowrap; }
    .form-progress { min-width: 0; flex: 1 1 auto; margin-right: 0; }
    .sticky-actions-hint { display: none; }
    /* And the total itself on one line: the full sentence wrapped to
       three, making the bar 97px tall (audit N4). The short copy is the
       same count; the full sentence stays for a screen reader. */
    .form-progress-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .form-progress-text .fp-long {
      position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
    }
    .form-progress-text .fp-short { display: inline; }
  }

  /* Jump to section from the pinned bar (decision N3-a). At ≤768px the
     strip above is static and scrolls away, so the bar's count becomes a
     button opening the section list; on a wide screen the side strip
     sticks and this stays hidden. One line at 375px has room for two
     things, not three, so the count *is* the button (a separate one
     squeezed it to 22px), its bar moves to a hairline along the top edge
     of the pinned bar, and the gap and Generate's side padding tighten a
     step. The wrapper stays
     position:static so the panel is placed against the sticky bar itself
     — full bar width, opening upward — rather than against the button. */
  .jump-menu { display: none; }
  @media (max-width: 768px) {
    .sticky-actions { gap: var(--space-2); }
    .sticky-actions > button { padding-left: var(--space-3); padding-right: var(--space-3); }
    .sticky-actions .form-progress-text { display: none; }
    .sticky-actions .form-progress-track {
      position: absolute; top: var(--space-1); left: var(--space-4); right: var(--space-4);
    }
    .jump-menu { display: block; min-width: 0; }
    .jump-btn {
      display: inline-flex; align-items: center; gap: var(--space-1); max-width: 100%;
      padding-left: var(--space-2); padding-right: var(--space-2);
      font-size: var(--text-sm); font-weight: 600;
    }
    .jump-btn-text { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .jump-chevron { flex: none; width: 0.5rem; height: 0.5rem; transition: transform 0.15s ease; }
    .jump-btn[aria-expanded="true"] .jump-chevron { transform: rotate(180deg); }
    .jump-menu-panel {
      position: absolute; left: 0; right: 0; bottom: calc(100% + var(--space-2));
      max-height: 60vh; overflow-y: auto;
      background: var(--surface); border: 1px solid var(--rule-strong);
      border-radius: var(--radius-md); padding: var(--space-1);
      box-shadow: var(--shadow-up);
    }
    .jump-menu-title {
      padding: var(--space-1) var(--space-3); font-size: var(--text-xs); font-weight: 600; color: var(--slate);
    }
    .jump-menu-panel ol { list-style: none; margin: 0; padding: 0; }
    .jump-item {
      display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
      min-height: 2.75rem; padding: var(--space-1) var(--space-3);
      border-radius: var(--radius-md); border-left: 2px solid transparent;
      color: var(--ink); text-decoration: none; font-size: var(--text-sm); font-weight: 600;
    }
    .jump-item[aria-current="location"] { background: var(--accent-soft); border-left-color: var(--ink); }
    .jump-item-count { font-size: var(--text-xs); font-weight: 400; color: var(--slate); white-space: nowrap; }
    /* A tick as well as the colour, as on the strip. */
    .jump-item.done .jump-item-count { color: var(--good); }
    .jump-item.done .jump-item-count::before { content: "\2713\00a0"; }
  }
  @media (prefers-reduced-motion: reduce) { .jump-chevron { transition: none; } }

  /* The review page's ⋮ menu. When the header wraps on a phone the
     button dropped to the left edge, and the menu — anchored to its right
     edge — opened off-screen (x -76). Kept at the right however the
     header wraps, so the right-anchored menu always opens inward. */
  .disc-head-menu { margin-left: auto; align-self: flex-start; }

  /* "Known gaps in this draft" as a disclosure: open on a wide screen,
     closed on a phone, where it pushed the first field below the fold. */
  .known-gaps > summary {
    cursor: pointer; display: flex; align-items: center; min-height: 1.5rem; margin: 0;
  }
  .known-gaps[open] > summary { margin-bottom: var(--space-2); }
  @media (pointer: coarse) { .known-gaps > summary { min-height: 2.75rem; } }

  /* ── Disclosure status pill ──
     The mockup's status marker for the disclosure screens: a hairline
     outline pill with a leading dot, rather than the soft-filled
     .status-badge the rest of the app uses. It reads as one small enum
     (a filing is a draft, or it is signed off and ready), so it carries
     the state in the dot's HUE — amber for a draft in progress, green
     once it's ready — and keeps the text a neutral --slate so the colour
     isn't the only cue. The border is decoration around a label, not the
     outline of a control, so its colour-mix is outside 1.4.11's 3:1. */
  .disc-pill { border-color: var(--rule-strong); color: var(--slate); }
  .disc-pill::before { content: ""; width: 0.44rem; height: 0.44rem; border-radius: 50%; background: var(--warn); flex: none; }
  .disc-pill.ready { border-color: color-mix(in srgb, var(--good) 55%, transparent); }
  .disc-pill.ready::before { background: var(--good); }
  /* Filed: done and read-only, so the dot goes neutral rather than
     louder — the green "ready" is the last state that asks anything of
     the user. The word "Filed" carries it, not the hue. */
  .disc-pill.filed::before { background: var(--slate); }

  /* ── Prepared-filings list ──
     The mockup renders the filings as a clean list, not a bordered
     table-in-a-panel: a strong hairline over an uppercase micro-header,
     then one filing per row divided only by a rule. It stays a real
     <table> (screen-reader columns, and the same rebuild idiom as
     .dir-table) — only the chrome changes. The form label is the row's
     primary link into review; the trailing cell keeps the app's own
     Review link and ⋮ row menu, which the mockup has no equivalent for. */
  .disc-table thead th {
    font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.06em;
    font-weight: 600; color: var(--slate); padding: var(--space-2) var(--space-2);
    border-top: 1px solid var(--rule-strong); border-bottom: 1px solid var(--rule);
  }
  .disc-table td { padding: var(--space-3) var(--space-2); vertical-align: middle; }
  .disc-table th:first-child, .disc-table td:first-child { padding-left: 0; }
  .disc-table th:last-child, .disc-table td:last-child { padding-right: 0; text-align: right; }
  .disc-form-link { font-weight: 600; color: var(--ink); }
  /* "Renewal · 2027–2028" under a renewal's form name. */
  .disc-kind-line { font-size: var(--text-xs); color: var(--slate); margin-top: var(--space-1); }
  .disc-table .disc-kind-line { white-space: nowrap; }  /* one line in a narrow Form column */
  /* The one form's name, where a one-choice dropdown was (Y19): sized as
     a card heading, under the page's h1 rather than louder than it. */
  .disc-form-name { font-size: var(--text-base); font-weight: 600; margin: 0; }
  /* The start form's first-registration / renewal choice. */
  .disc-kind-option {
    display: flex; align-items: flex-start; gap: var(--space-2);
    font-size: var(--text-sm); margin-top: var(--space-2);
  }
  .disc-kind-option input { width: auto; margin-top: var(--space-1); }
  .disc-kind-option .field-hint { display: block; }
  @media (hover: hover) { .disc-form-link:hover { color: var(--gold); } }
  .disc-row-actions { display: inline-flex; align-items: center; gap: var(--space-3); justify-content: flex-end; }
  .disc-review-link { font-size: var(--text-xs); color: var(--slate); white-space: nowrap; }
  @media (hover: hover) { .disc-review-link:hover { color: var(--gold); } }
  /* "Add your firm's legal name →" under the new-draft form was a 15px
     line of text — the only way forward from that state. 24px, and 44px
     for a finger, like the app's other targets. */
  .disc-fix-link { display: inline-flex; align-items: center; min-height: 1.5rem; }
  @media (pointer: coarse) { .disc-fix-link { min-height: 2.75rem; } }

  /* ── Compliance v5 (docs/design-system: Card, data, forms, actions) ──
     Laid over the rules above for the Compliance area only — Due dates
     (COMPLIANCE_BODY), Positions by quarter, Prepared forms and the
     review page — each wrapped in .compliance-page. .disc-table, the
     pills and the section rail are shared with Documents > Client
     reports and the client list, which keep theirs. Serif page and card
     titles; white 18px cards on a hairline with no shadow; each list a
     flush card whose table has a tinted, rounded head band in sentence
     case; bill numbers mono navy; status and due chips borderless tinted
     pills; the Prepared forms counts as StatTiles; the section rail's
     current item a raised white pill. Buttons and fields keep the app's
     own shape for now (that changes app-wide in one pass); the --gold
     focus ring, the pills' one base shape, the position chips' borders
     and glyphs (P2-31) and the 44px touch floor stay. */
  .compliance-page .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em;
  }
  .compliance-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }

  /* Cards: hairline edge, no shadow, serif titles. A card's own title is
     an h2.section, a .bill-title or (Positions by quarter) the client's
     name; "Preview and sign off" sits over its cards the same way. */
  .compliance-page .card { border-color: var(--rule); box-shadow: none; padding: var(--space-4); }
  .compliance-page h2.section, .compliance-page .card > .bill-title, .compliance-page .card > h2,
  .compliance-page .disc-form-name {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500;
    line-height: 1.2; letter-spacing: -0.01em;
  }
  .compliance-page .card > h2.section { margin: 0 0 var(--space-2); }
  .compliance-page .card > h2 > a { color: var(--ink); }
  @media (hover: hover) { .compliance-page .card > h2 > a:hover { color: var(--link); } }
  .compliance-page .card .sub, .compliance-page .bill-desc { color: var(--ink-2); }
  .compliance-page .field-label { font-weight: 500; }
  /* The global form and .sub margins left a blank band at the foot of a
     card; the card's own padding ends it. */
  .compliance-page .card > form, .compliance-page .card > :last-child { margin-bottom: 0; }

  /* A flush card holding a list: a small inset, the title on its own
     padding, the table's tinted head band as the only divider. */
  .compliance-page .compliance-card { padding: var(--space-1); }
  .compliance-page .compliance-card > h2 { margin: 0; padding: var(--space-3) var(--space-3) var(--space-2); }
  /* The ledgers' add forms (Expenses, Contributions) and the quarter
     picker above them. With only the bare form rule they were a ragged
     flex wrap flush to the card's edge, the submit stretched into a tall
     slab beside the officials box. Now an even grid of fields inset like
     the card's rows, the officials box a full-width group of its own,
     and the submit an ordinary button under it all. */
  .compliance-page .compliance-card > label { display: block; padding: var(--space-2) var(--space-3) var(--space-1); }
  .compliance-page .compliance-card > form {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
    gap: var(--space-3) var(--space-4); align-items: end; margin: 0; padding: var(--space-1) var(--space-3) var(--space-3);
  }
  .compliance-page .compliance-card > form label { display: flex; flex-direction: column; min-width: 0; }
  .compliance-page .compliance-card > form input, .compliance-page .compliance-card > form select { width: 100%; min-width: 0; }
  .compliance-page .compliance-card > form > :is(fieldset, .ban-warning) { grid-column: 1 / -1; }
  .compliance-page .compliance-card > form > fieldset {
    margin: 0; padding: var(--space-2) var(--space-3) var(--space-3); min-width: 0;
    border: 1px solid var(--rule); border-radius: var(--radius-md);
  }
  .compliance-page .compliance-card > form > fieldset > legend { padding: 0 var(--space-1); }
  .compliance-page .expense-person {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
    gap: var(--space-3) var(--space-4); align-items: end; margin-bottom: var(--space-3);
  }
  .compliance-page .expense-person > p { grid-column: 1 / -1; margin: 0; }
  .compliance-page .compliance-card > form > button[type="submit"] { grid-column: 1 / -1; justify-self: start; }
  /* The quarterly report (Form 625): each part a section inset like the
     card's rows and split by a hairline, titled like every other card
     title (the serif, not a heavy sans h2), its Copy the in-card small
     button. Its notes and lists sat flush to the card's edge. */
  .compliance-page #report > :is(.sub, .empty) { padding: 0 var(--space-3); }
  .compliance-page .report-part { padding: var(--space-3); border-top: 1px solid var(--rule); }
  .compliance-page .report-part h2 {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; letter-spacing: -0.01em; margin: 0 0 var(--space-2);
  }
  .compliance-page .report-part h3 { font-size: var(--text-base); font-weight: 600; margin: var(--space-3) 0 var(--space-1); }
  .compliance-page .report-part ul { margin: 0; padding-left: var(--space-5); }
  .compliance-card .empty, .compliance-card > .due-none, .compliance-card [role="status"] {
    margin: 0; padding: 0 var(--space-3) var(--space-3);
  }
  .compliance-card .disc-table { border-collapse: separate; border-spacing: 0; }
  .compliance-card .disc-table thead th {
    border-top: 0; border-bottom: 0; background: var(--paper-50); padding: var(--space-2);
    font-size: var(--text-2xs); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
  }
  .compliance-card .disc-table th:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
  .compliance-card .disc-table th:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
  .compliance-card .disc-table th:first-child, .compliance-card .disc-table td:first-child { padding-left: var(--space-3); }
  .compliance-card .disc-table th:last-child, .compliance-card .disc-table td:last-child { padding-right: var(--space-3); }
  .compliance-card .disc-table tbody tr:last-child td { border-bottom: 0; }
  .compliance-card .disc-table td { color: var(--ink); }
  .compliance-card .disc-table td.date, .compliance-card .disc-review-link { color: var(--ink-2); }
  .compliance-card .disc-form-link { font-weight: 500; }
  /* Compliance > Contributions: the lobbyist-ban warning (Gov. Code §85702)
     on a row and beside the add form. It warns; it never blocks. */
  .compliance-page .ban-warning { color: var(--warn); background: var(--warn-soft); border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); margin: var(--space-2) 0; }
  @media (hover: hover) {
    .compliance-card .disc-form-link:hover, .compliance-card .disc-review-link:hover { color: var(--link); }
  }

  /* BillNumber: mono navy, ahead of the title column. */
  .compliance-page .compliance-bill-number { font-family: var(--mono); font-weight: 400; color: var(--link); white-space: nowrap; }
  @media (hover: hover) { .compliance-page .compliance-bill-number:hover { color: var(--ink); } }

  /* Chips: borderless tinted pills that say their state in words. A
     draft is amber, signed off green, filed neutral (it asks nothing
     more of anyone); a due date close or past keeps its amber / red. */
  .compliance-page .disc-pill { border-color: transparent; background: var(--warn-soft); color: var(--warn); }
  .compliance-page .disc-pill.ready { border-color: transparent; background: var(--good-soft); color: var(--good); }
  .compliance-page .disc-pill.filed { background: var(--paper-250); color: var(--ink-2); }
  .compliance-page .due-chip { border-color: transparent; }

  /* Prepared forms' counts as StatTiles: a ringed icon tile beside the
     number, a hairline edge, no lift. */
  .compliance-stats { grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); gap: var(--space-3); }
  .compliance-stats .stat-card {
    display: grid; grid-template-columns: 2.75rem minmax(0, 1fr); column-gap: var(--space-3); align-items: center;
    border-color: var(--rule); box-shadow: none; padding: var(--space-4);
  }
  @media (hover: hover) { .compliance-stats .stat-card:hover { box-shadow: none; } }
  .compliance-stats .icon-tile {
    grid-row: 1 / span 3; width: 2.75rem; height: 2.75rem; border-radius: var(--radius-md);
    box-shadow: inset 0 0 0 1px var(--rule);
  }
  .compliance-stats .icon-tile svg { width: 1.125rem; height: 1.125rem; }
  .compliance-stats .stat-value { letter-spacing: -0.02em; line-height: 1; }
  .compliance-stats .stat-label { margin-top: var(--space-1); font-size: var(--text-sm); color: var(--ink); }
  .compliance-stats .stat-foot { margin-top: var(--space-0); font-size: var(--text-2xs); color: var(--muted); }

  /* Positions by quarter: the Quarter Select with its label to its left. */
  .compliance-page .history-filter { flex-direction: row; align-items: center; gap: var(--space-2); font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); }

  /* The review page. A client's row inside the Clients card is an inset
     row (12px corners, hairline) titled in the sans, not a second card. */
  .compliance-page .client-row-card.card { border-radius: var(--radius-md); }
  .compliance-page .client-row-head .bill-title { font-size: var(--text-base); font-weight: 600; }
  .compliance-page .review-banner { border-color: transparent; }
  .compliance-page .review-banner-head { font-weight: 600; }
  /* The section rail: 10px items; the current one a raised white pill
     rather than a tint with an ink bar. On a phone the rail's chips are
     borderless tints, the current one raised the same way. */
  .compliance-page .section-nav-item { border-radius: var(--radius-sm); font-weight: 500; color: var(--ink-2); }
  .compliance-page .section-nav-item[aria-current="location"] {
    background: var(--surface); border-color: transparent; box-shadow: var(--shadow-nav-active); color: var(--ink);
  }
  .compliance-page .jump-item { border-radius: var(--radius-sm); font-weight: 500; }
  .compliance-page .jump-item[aria-current="location"] { border-left-color: transparent; }
  @media (max-width: 768px) {
    .compliance-page .section-nav-item { border-color: transparent; border-radius: var(--radius-pill); background: var(--paper-250); }
    .compliance-page .section-nav-item[aria-current="location"] { background: var(--surface); }
  }

  @media (max-width: 640px) {
    .compliance-page .page-head h1 { font-size: var(--text-xl); }
    .compliance-page .history-filter { flex-wrap: wrap; }
    .compliance-stats .stat-card { grid-template-columns: auto minmax(0, 1fr); row-gap: 0; padding: var(--space-3); }
    .compliance-stats .icon-tile { grid-row: 1; width: 2.25rem; height: 2.25rem; }
    .compliance-stats .stat-label, .compliance-stats .stat-foot { grid-column: 1 / -1; }
    .compliance-stats .stat-label { margin-top: var(--space-2); }
    /* A list's table becomes one block per row, so nothing scrolls
       sideways: no column head, each row's cells stacked under its
       first, and a muted label leading the cells a head used to name
       (data-label). */
    .compliance-card .disc-table, .compliance-card .disc-table tbody,
    .compliance-card .disc-table tr, .compliance-card .disc-table td { display: block; }
    .compliance-card .disc-table thead { display: none; }
    .compliance-card .disc-table tr { padding: var(--space-3); border-bottom: 1px solid var(--rule); }
    .compliance-card .disc-table tbody tr:last-child { border-bottom: 0; }
    .compliance-card .disc-table td, .compliance-card .disc-table td:first-child, .compliance-card .disc-table td:last-child {
      min-width: 0; padding: 0; border-bottom: 0; text-align: left; white-space: normal;
    }
    .compliance-card .disc-table td + td { margin-top: var(--space-1); }
    .compliance-card .disc-table td[data-label] { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--space-2); }
    .compliance-card .disc-table td[data-label]::before {
      content: attr(data-label); font-size: var(--text-2xs); font-weight: 500; color: var(--muted);
    }
  }

  /* ── Registration form: address rows ──
     City, State and ZIP shared one row at every width, which left the ZIP
     box 63px wide on a phone. There, City takes its own row and State and
     ZIP share the next. */
  .reg-city { flex: 2 1 12rem; }
  .reg-state, .reg-zip { flex: 1 1 5rem; }
  @media (max-width: 640px) {
    .reg-city { flex: 1 1 100%; }
  }

  /* ── Registration form v5 (docs/design-system: Card, forms) ──
     /registration (REGISTRATION_BODY), scoped to .registration-page:
     the layout the template used to carry inline, then the design
     system over it. A serif page title; the form one white card on a
     hairline with no shadow; the two address heads in the card serif;
     labels at 500 and "(optional …)" asides in --muted; the registrant
     radios navy when chosen (Checkbox), each hint under its label.
     Buttons, fields and the switch keep the app's own shape (that
     changes app-wide in one pass), as does the --gold focus ring. */
  .registration-page .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em; text-wrap: balance;
  }
  .registration-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }
  .registration-page .page-head a.sub { color: var(--link); }
  .registration-page .reg-card { border: 1px solid var(--rule); box-shadow: none; padding: var(--space-5); }
  .registration-page .reg-card .req-legend { color: var(--muted); }
  .registration-page #f { margin-bottom: 0; }
  .registration-page #f input:not([type="checkbox"]):not([type="radio"]) { width: 100%; }
  .registration-page .reg-full { flex: 1 1 100%; }
  .registration-page .reg-gap { margin-top: var(--space-4); }
  .registration-page .reg-date { flex: 1 1 10rem; }
  .registration-page .reg-st { text-transform: uppercase; }
  .registration-page .reg-mail-fields { flex: 1 1 100%; flex-wrap: wrap; gap: var(--space-2); }
  .registration-page .field-label { font-weight: 500; }
  .registration-page .field-optional, .registration-page .field-hint { color: var(--muted); }
  .registration-page .reg-section {
    margin: var(--space-4) 0 var(--space-2); font-family: var(--font-display); font-size: var(--text-md);
    font-weight: 500; line-height: 1.2; letter-spacing: -0.01em;
  }
  .registration-page .reg-choice {
    display: flex; align-items: flex-start; gap: var(--space-2); font-size: var(--text-sm); color: var(--ink);
  }
  .registration-page .reg-choice + .reg-choice { margin-top: var(--space-2); }
  .registration-page .reg-choice input { width: auto; margin-top: var(--space-1); accent-color: var(--link); }
  .registration-page .reg-choice .field-hint { display: block; }
  .registration-page .reg-same {
    display: inline-flex; align-items: center; gap: var(--space-2);
    font-size: var(--text-sm); margin-bottom: var(--space-3);
  }
  .registration-page .reg-submit { margin-top: var(--space-4); }
  @media (max-width: 640px) {
    .registration-page .page-head h1 { font-size: var(--text-xl); }
    .registration-page .reg-card { padding: var(--space-4); }
  }

  /* ── Saved searches ──
     The list above the search box (see saved_searches in schema.sql).
     Rows, not cards: each is a name, the query behind it, and a count of
     what's new — one line of content and one of detail. */
  .saved-row {
    display: flex; align-items: flex-start; gap: var(--space-3);
    padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--rule);
  }
  .saved-row:last-child { border-bottom: 0; }
  .saved-main { flex: 1; min-width: 0; }
  .saved-name {
    background: none; border: 0; padding: 0; cursor: pointer;
    font-size: var(--text-sm); font-weight: 600; color: var(--ink); text-align: left;
  }
  @media (hover: hover) { .saved-name:hover { color: var(--accent); text-decoration: underline; } }
  .saved-meta { font-size: var(--text-2xs); color: var(--slate); margin-top: var(--space-0); }

  /* ── Letters ──
     The editor is a document being read as a document, not a form field
     (whole-app audit Y17): the body face at body size with generous
     leading, on a calm card inset with page-like margins, and the letter
     column capped at 46rem so a wide window doesn't stretch lines to a
     length nobody proofreads accurately (~75 characters). It used to be
     monospace at half the window with the right half blank; that half
     now holds "Who handles this". */
  .letter-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-4); margin-top: var(--space-4); }
  .letter-layout > * { min-width: 0; max-width: 46rem; }
  /* Two columns only when there is a routing panel to put in the second
     — a hidden one (no match yet, or the lookup failed) leaves the letter
     alone rather than beside an empty column. The panel is first in the
     source (the recipient is decided before the argument is written, and
     on a phone it stacks above the letter, folded to one name — N3-b);
     here it is placed to the right. */
  @media (min-width: 1200px) {
    .letter-layout:has(> .letter-aside:not([hidden])) {
      grid-template-columns: minmax(0, 46rem) minmax(16rem, 24rem); align-items: start;
    }
    .letter-layout:has(> .letter-aside:not([hidden])) > .letter-main { grid-column: 1; grid-row: 1; }
    .letter-layout > .letter-aside { grid-column: 2; grid-row: 1; max-width: none; }
    /* A 24rem column is too narrow for a name beside three buttons. */
    .letter-aside .routing-row { flex-direction: column; }
  }
  textarea.letter-body {
    display: block; width: 100%;
    font-family: var(--font-sans); font-size: var(--text-base); line-height: 1.7; white-space: pre-wrap;
    background: var(--surface); border: 1px solid var(--rule-card); border-radius: var(--radius-lg);
    padding: var(--space-5) var(--space-6);
    /* Vertical only: dragging it wider broke the page on a phone. Its
       height follows the text (fitBody() in LETTER_EDIT_BODY), so the
       page is the one scroller rather than a box scrolling inside it. */
    resize: vertical; overflow-y: hidden;
  }
  /* The subject line: a textarea only so it can wrap and grow
     (fitSubject() in LETTER_EDIT_BODY); it reads as the text field it is. */
  textarea.letter-subject { resize: none; overflow-y: hidden; line-height: 1.4; }
  /* "Submit by" — the committee's deadline and the submitted mark, atop
     the letter. Wraps to a stack on a phone. */
  .letter-deadline-body { padding: var(--space-4); }
  .letter-deadline-row { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-5); align-items: flex-end; }
  .letter-due-field { display: flex; flex-direction: column; gap: var(--space-1); }
  .letter-due-field > label { margin: 0; }
  .letter-due-input { display: flex; align-items: center; gap: var(--space-3); }
  .letter-due-suggest { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
  .letter-due-suggest[hidden], .letter-due-input [hidden] { display: none; }
  .letter-due-suggest .sub, .letter-due-field p.sub { margin: 0; font-size: var(--text-2xs); }
  .letter-submit {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3);
    margin-top: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--rule);
  }
  .letter-portal-link { font-size: var(--text-xs); font-weight: 600; }
  /* Where a letter stands with the committee — the editor and the Letters
     list. Each state carries its own words ("Overdue — due …"); colour
     is the second cue, not the only one. */
  .letter-pill { border-color: var(--rule-strong); color: var(--slate); }
  .letter-pill.soon { background: var(--warn-soft); color: var(--warn); border-color: currentColor; }
  .letter-pill.over { background: var(--error-soft); color: var(--error); border-color: currentColor; }
  .letter-pill.submitted { background: var(--good-soft); color: var(--good); }
  /* Phone: the Letters list as stacked cards, as the directory table
     does — six columns scrolled sideways put the status out of sight. */
  @media (max-width: 900px) {
    .letters-table, .letters-table tbody, .letters-table tr, .letters-table td { display: block; width: auto; }
    .letters-table thead { display: none; }
    .letters-table tr { border-bottom: 1px solid var(--rule-strong); padding: var(--space-3) var(--space-4); }
    .letters-table tr:last-child { border-bottom: 0; }
    .letters-table td { border: 0; padding: var(--space-1) 0; }
    .letters-table td.col-edited::before { content: "Last edited "; color: var(--slate); }
    .letters-table td:last-child .cell-actions { justify-content: flex-start; }
  }
  /* On a phone the page margins shrink so the measure stays ~40
     characters rather than losing a third of the width to padding. */
  @media (max-width: 767px) {
    textarea.letter-body { padding: var(--space-3) var(--space-4); line-height: 1.6; }
  }
  /* Printing a letter should produce the letter, not the app around it
     — the sidebar, topbar and every control are furniture on paper. */
  @media print {
    .app-sidebar, .app-topbar, .card-actions, .page-head, .panel-head .sub, #error,
    .skip-link { display: none !important; }
    .app-main { padding: 0 !important; }
    .letter-aside { display: none !important; }
    .letter-deadline { display: none !important; }
    .letter-layout { display: block; }
    textarea.letter-body {
      border: 0; padding: 0; max-width: none; font-size: var(--text-base);
      font-family: Georgia, 'Times New Roman', serif; height: auto;
    }
  }
  /* The letter editor's "signed with… from Settings" note (workflow audit
     C2) is about the app, not part of the letter. */
  @media print { .letter-signature-source, .letter-signons, .letter-ai { display: none !important; } }
  /* Draft with AI (ai_letter.py): the "AI draft — review before sending"
     label over the text, in the stale-save banner's warning tint. */
  .letter-ai > p.sub { margin: 0 0 var(--space-3); }
  .letter-ai-note {
    background: var(--warn-soft); border-radius: var(--radius-md);
    padding: var(--space-3) var(--space-4); margin: 0 0 var(--space-3);
  }
  .letter-ai-note p { margin: 0; font-size: var(--text-sm); }
  .letter-ai-note .linklike { margin-top: var(--space-2); }
  /* Coalition sign-ons (Oct 7 audit P6): the signed names print under the
     letter; the panel that records them doesn't. */
  .letter-signature-block {
    margin: var(--space-2) 0 0; white-space: pre-wrap;
    font-family: Georgia, 'Times New Roman', serif;
  }
  .signon-list { list-style: none; margin: 0 0 var(--space-3); padding: 0; }
  .signon-row { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; padding: var(--space-1) 0; }
  .signon-name { flex: 1 1 12rem; }
  .signon-add { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
  .signon-rep { flex: 1 1 10rem; min-width: 0; }
  /* Ready to submit (letter_packet.py): the committee's deadline rule,
     the pre-flight checks, the portal steps and the email. Each check
     carries its word (OK / Fix / Check) — colour is the second cue. Never
     printed: the printed page is the letter. */
  .letter-packet { margin-top: var(--space-4); }
  .letter-packet .panel-head .card-actions a { white-space: nowrap; }
  .letter-packet-body { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
  .letter-packet-body p { margin: 0; }
  .letter-packet [hidden] { display: none !important; }
  .packet-heading { font-size: var(--text-sm); font-weight: 700; margin: var(--space-2) 0 0; }
  .packet-deadline { font-size: var(--text-sm); }
  .packet-rule-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); margin-top: var(--space-2); }
  .packet-rule-form {
    display: flex; flex-direction: column; gap: var(--space-2); max-width: 28rem;
    padding: var(--space-3); border: 1px solid var(--rule); border-radius: var(--radius-lg);
  }
  .packet-rule-form .field-label { margin: 0; }
  .packet-rule-check { display: flex; align-items: center; gap: var(--space-2); }
  .packet-checks, .packet-sent { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
  .packet-check { display: flex; gap: var(--space-3); align-items: baseline; font-size: var(--text-sm); }
  .packet-check-mark {
    flex: none; min-width: 3.25rem; text-align: center; font-size: var(--text-2xs); font-weight: 700;
    padding: var(--space-0) var(--space-2); border-radius: var(--radius-pill); background: var(--paper-250); color: var(--ink-2);
  }
  .packet-check.ok .packet-check-mark { background: var(--good-soft); color: var(--good); }
  .packet-check.fix .packet-check-mark { background: var(--error-soft); color: var(--error); }
  .packet-check.check .packet-check-mark { background: var(--warn-soft); color: var(--warn); }
  .packet-steps { margin: 0 0 var(--space-3); padding-left: var(--space-5); display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--text-sm); }
  .packet-sent { font-size: var(--text-sm); }
  .email-confirm { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-2) var(--space-3); margin: 0 0 var(--space-3); font-size: var(--text-sm); }
  .email-confirm dt { color: var(--slate); }
  .email-confirm dd { margin: 0; overflow-wrap: anywhere; font-weight: 600; }
  @media print { .letter-packet { display: none !important; } }

  /* ── Letters v5 (docs/design-system: Card, data, forms, actions) ──
     Laid over the rules above for the Letters list (.letters-page) and
     the letter editor (.letter-editor) only — the client page's Letters
     tab, the bill report's letter picker and the client report's
     "sent" pill keep theirs. Serif page and card titles; white 18px
     cards on a hairline with no shadow and no header rule; the list a
     flush card whose table has a tinted, rounded head band in sentence
     case and rows that tint on hover; bill numbers mono navy; a
     letter's status a borderless tinted chip. Buttons and fields keep
     the app's own shape for now (button shape is to change app-wide in
     one pass); the pills keep the one base shape; the --gold focus ring
     and 44px touch floor stay. The editor's rules are screen-only, so
     the printed letter is exactly what it was. */
  .letters-page .page-head h1, .letter-editor .page-head h1 {
    font-family: var(--font-display); font-weight: 500; font-size: var(--text-display);
    line-height: 1.05; letter-spacing: -0.015em;
  }
  .letters-page .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--space-1); max-width: 46rem; text-wrap: pretty; }

  /* Start a letter: a card with a serif title. */
  .letters-page .card.letters-start { border-color: var(--rule); box-shadow: none; padding: var(--space-4); }
  .letters-start .bill-title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; line-height: 1.2; letter-spacing: -0.01em;
  }
  .letters-start .sub { color: var(--ink-2); margin-bottom: 0; }
  .letters-start form { margin-bottom: 0; }

  /* Your letters: a flush Card around the table. */
  .panel.letters-card { border-color: var(--rule); box-shadow: none; }
  .letters-card .panel-head { border-bottom: 0; padding: var(--space-4) var(--space-4) var(--space-3); }
  .letters-card .panel-head .title {
    font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; letter-spacing: -0.01em;
  }
  .letters-card .letters-table td > a { font-weight: 500; color: var(--ink); }
  @media (hover: hover) { .letters-card .letters-table td > a:hover { color: var(--link); } }
  .letters-card .letters-table .sub { color: var(--ink-2); margin: var(--space-0) 0 0; }
  .letters-card .letters-table .rereview-note { font-weight: 500; }
  /* BillNumber: mono navy, regular weight, as typed. */
  .letters-card td.col-bill {
    font-family: var(--mono); font-size: var(--text-xs); font-weight: 400; text-transform: none; color: var(--link);
  }
  .letters-card td.date { color: var(--ink-2); }
  @media (min-width: 901px) {
    .letters-card .letters-table { border-collapse: separate; border-spacing: 0; }
    .letters-card .letters-table th {
      border-top: 0; border-bottom: 0; background: var(--paper-50); padding-top: var(--space-2); padding-bottom: var(--space-2);
      font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--muted);
    }
    .letters-card .letters-table th:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
    .letters-card .letters-table th:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
    .letters-card .letters-table th:first-child, .letters-card .letters-table td:first-child { padding-left: var(--space-3); }
    .letters-card .letters-table th:last-child, .letters-card .letters-table td:last-child { padding-right: var(--space-2); }
    .letters-card .letters-table tbody tr:last-child td { border-bottom: 0; }
    /* The letter's own column is the row's title: "Last edited" may
       wrap after its date, so the letter isn't squeezed to a word a line
       by the columns that can't. */
    .letters-card .letters-table td.col-edited { white-space: normal; min-width: 7rem; }
    @media (hover: hover) {
      .letters-card .letters-table tbody tr:hover { background: none; }
      .letters-card .letters-table tbody tr:hover td { background: var(--paper-50); }
    }
  }
  .letters-card div:has(> .letters-table) { padding: 0 var(--space-2) var(--space-2); }
  /* The phone card: a hairline between letters, the label in sentence case. */
  @media (max-width: 900px) {
    .letters-card div:has(> .letters-table) { padding: 0; }
    .letters-card .letters-table tr { border-bottom-color: var(--rule); }
    .letters-card .letters-table td.col-edited::before { color: var(--muted); }
  }

  /* A letter's status: a borderless tinted chip (the word carries it). */
  :is(.letters-page, .letter-editor) .letter-pill { border-color: transparent; }
  :is(.letters-page, .letter-editor) .letter-pill:not(.soon, .over, .submitted) { background: var(--paper-250); color: var(--ink-2); }

  @media screen {
    .letter-editor .page-head .sub { font-size: var(--text-sm); color: var(--ink-2); max-width: 46rem; text-wrap: pretty; }
    .letter-editor .page-head a.sub { font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); }
    @media (hover: hover) { .letter-editor .page-head a.sub:hover { color: var(--ink); } }
    .letter-editor .letter-title-number {
      font-family: var(--mono); font-size: var(--text-xl); font-weight: 500; letter-spacing: 0; color: var(--link);
    }
    .letter-editor :is(.panel, .card) { border-color: var(--rule); box-shadow: none; }
    /* The subject line's card lines its label up with the panels' titles. */
    .letter-editor .card { padding: var(--space-4); }
    .letter-editor .panel-head { border-bottom: 0; align-items: flex-start; }
    .letter-editor .panel-head .title {
      font-family: var(--font-display); font-size: var(--text-md); font-weight: 500; line-height: 1.2; letter-spacing: -0.01em;
    }
    .letter-editor .panel-head .sub { color: var(--ink-2); max-width: 40rem; text-wrap: pretty; }
    .letter-editor .letter-deadline-body { padding-top: 0; }
    .letter-editor #letter-received { margin: var(--space-2) 0 0; }
    /* Field labels (TextInput / TextArea): 12px, medium, secondary ink. */
    .letter-editor label.sub { font-size: var(--text-xs); font-weight: 500; color: var(--ink-2); }
    .letter-editor .letter-portal-link { font-weight: 500; }
    .letter-editor textarea.letter-body { border-color: var(--rule); }
    /* Who handles this: field-label heads, medium names. */
    .letter-editor .routing-heading { font-weight: 500; color: var(--muted); }
    .letter-editor .routing-row strong { font-weight: 500; }
    .letter-editor .routing-other > summary { color: var(--ink-2); }
  }
  @media screen and (max-width: 640px) {
    .letter-editor .page-head h1 { font-size: var(--text-xl); }
    .letter-editor .letter-title-number { font-size: var(--text-lg); }
  }
  @media (max-width: 640px) {
    .letters-page .page-head h1 { font-size: var(--text-xl); }
  }

  /* ── Digest settings (P2-30) ──
     The one settings form in this app, so it gets a shape rather than a
     component library: a label column that lines up with the read-only
     rows already on Profile, and checkbox rows that stay one tap target
     each on a phone (the label wraps the input, so the caption is part
     of the hit area, not decoration next to it). */
  .pref-group { padding: var(--space-4) var(--space-4); border-bottom: 1px solid var(--rule); }
  .pref-group:last-of-type { border-bottom: 0; }
  .pref-group > .pref-label {
    font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.04em;
    text-transform: uppercase; color: var(--slate); margin-bottom: var(--space-2);
  }
  .pref-hint { font-size: var(--text-xs); color: var(--slate); margin-top: var(--space-2); }
  .pref-check {
    display: flex; align-items: baseline; gap: var(--space-2);
    padding: var(--space-2) 0; cursor: pointer;
  }
  .pref-check input { margin: 0; flex: none; accent-color: var(--accent-solid); }
  .pref-check .pref-check-text { font-size: var(--text-sm); }
  .pref-check .pref-check-sub { font-size: var(--text-xs); color: var(--slate); margin-top: var(--space-0); }
  /* iOS-style track+knob switch for boolean on/off settings (G5, mockup
     parity). It's a real <input type="checkbox"> restyled with
     appearance:none, so :checked, keyboard focus and every existing
     "input.checked" read in the page's JS keep working untouched — only
     the paint changes. Same track/knob dimensions as the dark-mode
     .theme-toggle-track above; the "on" colour is the app's gold accent
     (not --ink) to preserve the "on = gold" cue these checkboxes carried
     via accent-color, and because a saturated colour reads as enabled. */
  input.switch {
    -webkit-appearance: none; appearance: none; margin: 0; flex: none; cursor: pointer;
    position: relative; width: 1.9rem; height: 1.05rem;
    border-radius: var(--radius-pill); background: var(--rule);
    box-shadow: inset 0 0 0 1px var(--rule-strong);  /* see .theme-toggle-track */
    transition: background .15s ease;
  }
  input.switch::after {
    content: ""; position: absolute; top: 0.1rem; left: 0.1rem;
    width: 0.85rem; height: 0.85rem; border-radius: 50%;
    background: var(--surface); transition: transform .15s ease;
  }
  input.switch:checked { background: var(--accent-solid); }
  input.switch:checked::after { transform: translateX(0.85rem); }
  input.switch:focus-visible { outline: 2px solid var(--accent-solid); outline-offset: 2px; }
  /* A settings row carrying a switch reads label-left, control-right (the
     iOS list convention); row-reverse trailing the DOM-first input. */
  .pref-check.pref-toggle { flex-direction: row-reverse; justify-content: space-between; align-items: center; }
  /* Radios laid out as a column of rows rather than a segmented control:
     each option needs a sentence of consequence under it ("Monday
     mornings, covering the previous week"), and a segmented control has
     nowhere to put one. */
  .pref-radios { display: flex; flex-direction: column; gap: 0; }
  .pref-checks { display: flex; flex-direction: column; gap: 0; }
  .pref-group input[type="text"], .pref-group textarea { width: 100%; max-width: 30rem; }
  /* "Saved" is stated where the change was made, not in a corner toast:
     this form saves on submit, and the sentence has to be next to the
     button that caused it. */
  .pref-saved { font-size: var(--text-xs); color: var(--good); margin-left: var(--space-3); }
