/* ============================================================================
   Eduskunta AI-haku — Design System
   ----------------------------------------------------------------------------
   THIS IS THE ONLY FILE THAT DEFINES VISUAL STYLE.
   Read design.md (repo root) before editing. Golden rules:
     · Never write a raw hex outside the PRIMITIVES block.
     · Never write a raw px for spacing — use the --space-* scale.
     · Never add a CSS rule to index.html.
   ========================================================================== */


/* ============================================================================
   1. TOKENS — TIER 1: PRIMITIVES
   Raw palette. Do NOT reference these from component rules; use the semantic
   aliases in section 2 so the theme can be remapped in one place.
   ========================================================================== */
:root{
  /* Parliamentary blue — the identity colour. --blue-600 is unchanged. */
  --blue-900:#0b2f57;
  --blue-800:#0d3762;
  --blue-700:#01458a;
  --blue-600:#0055a4;
  --blue-500:#2a7bd0;
  --blue-300:#93b9e3;
  --blue-200:#c9dcf2;
  --blue-100:#e8f0fa;
  --blue-050:#f0f5fb;

  /* Warm paper — the background identity. --paper-100 is unchanged. */
  --paper-050:#faf9f5;
  --paper-100:#f4f2ec;
  --paper-200:#eeebe1;
  --sand-300:#e5e1d6;

  /* Cool neutrals for text and lines. */
  --slate-900:#1c2430;
  --slate-700:#3c4656;
  --slate-500:#5b6270;   /* replaces #6b7280 — 5.48:1 on paper, was 4.32:1 */
  --slate-300:#cfd6e0;
  --slate-100:#eef0f4;

  /* Accent */
  --gold-600:#b98a2e;

  /* Semantic hues */
  --red-600:#c0392b;
  --green-600:#2e773c;

  --white:#ffffff;

  /* Shadow tint: cool shadow over warm paper. Keep the two in relation. */
  --shadow-rgb:20,25,40;
}


/* ============================================================================
   2. TOKENS — TIER 2: SEMANTIC ALIASES
   What component rules reference. A future dark theme remaps only this block.
   ========================================================================== */
:root{
  --color-surface:          var(--paper-100);
  --color-surface-raised:   var(--white);
  --color-surface-sunken:   var(--paper-050);
  --color-surface-inset:    var(--paper-200);
  --color-surface-accent:   var(--blue-100);
  --color-surface-quote:    var(--blue-050);
  --color-surface-code:     var(--slate-100);
  --color-surface-header:   var(--blue-900);

  --color-ink:              var(--slate-900);
  --color-ink-soft:         var(--slate-700);
  --color-ink-muted:        var(--slate-500);
  --color-ink-inverse:      var(--white);

  --color-border:           var(--sand-300);
  --color-border-strong:    var(--slate-300);
  --color-border-accent:    var(--blue-200);

  --color-accent:           var(--blue-600);
  --color-accent-hover:     var(--blue-700);
  --color-accent-quiet:     var(--blue-100);
  --color-rule-gold:        var(--gold-600);

  --color-focus-ring:       var(--blue-500);
  --color-danger:           var(--red-600);
  --color-success:          var(--green-600);

  /* Stance (hallitus / oppositio). Deliberately hueless: the party badge
     beside it already carries a colour, and a second saturated pill would
     fight it — worse, red-or-green for a political side reads as a verdict.
     The meaning is carried by the Finnish word plus filled-vs-outline:
     in government is solid, outside it is an outline. */
  --color-stance-gov-bg:    var(--slate-700);
  --color-stance-gov-fg:    var(--color-ink-inverse);
  --color-stance-opp-fg:    var(--color-ink-soft);
  --color-stance-opp-border:var(--slate-500);

  /* Outcome (what happened to the bill being debated). Unlike stance this one
     IS a verdict — a bill passed or it did not — so the hue is carrying real
     information, not editorialising about a party. Two rules keep it from
     fighting the party badge and stance pill sitting beside it:
       * outline only. Both neighbours are solid; a third filled pill in one
         row turns the header into a row of buttons.
       * "rauennut" is neutral, never red. Lapsing is the ordinary end of a
         private member's bill (1 861 of 3 097 lakialoitteet), not a defeat,
         and colouring it as failure would misreport the corpus.
     Contrast, text on the four card surfaces (#ffffff / #faf9f5 / #f4f2ec /
     #eeebe1): pass 5.50 / 5.22 / 4.91 / 4.61, fail 5.44 / 5.16 / 4.86 / 4.56,
     neutral 6.13 / 5.82 / 5.48 / 5.14. All clear AA. */
  --color-outcome-pass:     var(--color-success);
  --color-outcome-fail:     var(--color-danger);
  --color-outcome-neutral:  var(--color-ink-muted);
}


/* ============================================================================
   3. TOKENS — SCALES
   ========================================================================== */
:root{
  /* Spacing — 4px base. Use for padding, margin and gap. Nothing else. */
  --space-0:  0;
  --space-05: .125rem;  /*  2px */
  --space-1:  .25rem;   /*  4px */
  --space-2:  .5rem;    /*  8px */
  --space-3:  .75rem;   /* 12px */
  --space-4:  1rem;     /* 16px */
  --space-5:  1.25rem;  /* 20px */
  --space-6:  1.5rem;   /* 24px */
  --space-8:  2rem;     /* 32px */
  --space-10: 2.5rem;   /* 40px */
  --space-12: 3rem;     /* 48px */

  /* Type. Small end (11–14px) is a UI-chrome ladder; the heading end steps
     at roughly 1.2. Base 15px sets the density of the whole app. */
  --text-2xs:  .6875rem; /* 11px — badges, usage bar, counts */
  --text-xs:   .75rem;   /* 12px — metadata */
  --text-sm:   .8125rem; /* 13px — secondary body */
  --text-md:   .875rem;  /* 14px — controls */
  --text-base: .9375rem; /* 15px — body */
  --text-lg:   1rem;     /* 16px — h3 */
  --text-xl:   1.125rem; /* 18px — h2 */
  --text-2xl:  1.5rem;   /* 24px — page title */

  --weight-normal: 400;
  --weight-medium: 500;
  --weight-semi:   600;
  --weight-bold:   700;

  --leading-tight:   1.25;
  --leading-normal:  1.6;
  --leading-relaxed: 1.7;

  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Radii — moderate and consistent: institutional, not consumer-soft. */
  --radius-xs:   4px;
  --radius-sm:   6px;
  --radius-md:   8px;
  --radius-lg:   12px;
  --radius-xl:   14px;
  --radius-full: 999px;
  --radius-bubble-user:      var(--radius-xl) var(--radius-xl) var(--radius-xs) var(--radius-xl);
  --radius-bubble-assistant: var(--radius-xs) var(--radius-xl) var(--radius-xl) var(--radius-xl);

  /* Elevation — separate by contrast and spacing first, shadow last. */
  --shadow-xs: 0 1px 2px rgba(var(--shadow-rgb),.05);
  --shadow-sm: 0 1px 3px rgba(var(--shadow-rgb),.07), 0 1px 2px rgba(var(--shadow-rgb),.04);
  --shadow-md: 0 2px 4px rgba(var(--shadow-rgb),.06), 0 8px 20px rgba(var(--shadow-rgb),.07);
  --shadow-lg: 0 4px 8px rgba(var(--shadow-rgb),.06), 0 16px 40px rgba(var(--shadow-rgb),.10);

  --border-width-thin:  1px;
  --border-width-thick: 2px;

  /* Motion — see the reduced-motion block at the end of this file. */
  --duration-fast: 120ms;
  --duration-base: 180ms;
  --duration-slow: 280ms;
  --ease-out: cubic-bezier(.2,0,0,1);

  /* Stacking — always use a step, never a raw number. */
  --z-base:    0;
  --z-sticky:  100;
  --z-overlay: 200;
  --z-modal:   300;
  --z-toast:   400;

  /* Layout. Chrome spans --layout-max; reading content is capped at
     --layout-measure so the line length stays comfortable. */
  --layout-max:     1040px;
  --layout-measure: 860px;
  --layout-gutter:  var(--space-5);
  /* --breakpoint-md: 800px — documented only; CSS cannot use a var in @media.
     The literal appears twice: section 15 (drawer) and section 17. Change all.
     --breakpoint-sm: 480px — likewise; used once, in section 17. */
}


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

body{
  font:var(--text-base)/var(--leading-normal) var(--font-sans);
  background:var(--color-surface);
  color:var(--color-ink);
  display:flex;
  flex-direction:column;
  min-height:100vh;
  /* dvh tracks the viewport as mobile browser chrome hides and shows; the vh
     line above stays as the fallback for browsers that don't know dvh. */
  min-height:100dvh;
  -webkit-font-smoothing:antialiased;
}

/* A component that sets its own `display` outranks the UA [hidden] rule and
   silently stays in the layout. .mp-drawer{display:flex} hit exactly that. */
[hidden]{display:none !important}

/* One focus treatment for everything interactive. */
:where(a,button,input,select,textarea,summary,[tabindex]):focus-visible{
  outline:var(--border-width-thick) solid var(--color-focus-ring);
  outline-offset:2px;
  border-radius:var(--radius-xs);
}

.wrap{
  width:100%;
  max-width:var(--layout-max);
  margin:0 auto;
  padding:0 var(--layout-gutter);
}
.wrap--main{padding-top:var(--space-4)}

/* Utilities — the only classes allowed to be applied ad hoc. */
.is-hidden{display:none !important}
.text-error{color:var(--color-danger)}
.text-muted{color:var(--color-ink-muted)}
/* Visually hidden but present for assistive tech — for required elements
   (a <legend>, an <output>'s label) whose text is redundant on screen but
   still needed as the accessible name/group label. Not display:none: that
   would remove it from the accessibility tree too. */
.sr-only{
  position:absolute;
  width:1px;
  height:1px;
  margin:-1px;
  padding:0;
  overflow:hidden;
  clip:rect(0,0,0,0);
  white-space:nowrap;
  border:0;
}


/* ============================================================================
   5. HEADER
   Near-flat institutional navy with a gold rule, replacing the old diagonal
   three-stop gradient — reads closer to a parliamentary publication.
   ========================================================================== */
.app-header{
  background:linear-gradient(180deg,var(--blue-900),var(--blue-800));
  border-bottom:var(--border-width-thick) solid var(--color-rule-gold);
  color:var(--color-ink-inverse);
  padding:var(--space-6) 0 var(--space-5);
}
.app-header__row{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--space-4);
}
.app-header__text{min-width:0}
.app-header__title{
  font-size:var(--text-2xl);
  font-weight:var(--weight-semi);
  line-height:var(--leading-tight);
  letter-spacing:-.01em;
}
.app-header__subtitle{
  margin-top:var(--space-1);
  font-size:var(--text-sm);
  opacity:.8;
}
/* #newChatBtn sits on the navy header; it needs its own shrink guard since
   .app-header__row is a flex row and .btn--ghost has no width of its own. */
.app-header__row .btn{flex-shrink:0}


/* ============================================================================
   6. TABS
   ========================================================================== */
.tabs{
  display:flex;
  max-width:var(--layout-measure);
  border-bottom:var(--border-width-thick) solid var(--color-border);
  margin-top:var(--space-5);
}
.tab-btn{
  background:none;
  border:none;
  border-bottom:var(--border-width-thick) solid transparent;
  margin-bottom:calc(var(--border-width-thick) * -1);
  padding:var(--space-3) var(--space-5);
  font:var(--weight-medium) var(--text-md)/1 var(--font-sans);
  color:var(--color-ink-muted);
  cursor:pointer;
  transition:color var(--duration-fast) var(--ease-out),
             border-color var(--duration-fast) var(--ease-out);
}
.tab-btn:hover{color:var(--color-ink)}
.tab-btn.is-active{
  color:var(--color-accent);
  border-bottom-color:var(--color-accent);
  font-weight:var(--weight-semi);
}

/* Pane visibility. A pane that needs a display mode other than `block` must
   declare it on `.<pane>.is-active` (same specificity, later in the file) —
   declaring it on the bare component class would override `display:none`
   and the pane would never hide. See .chat-pane.is-active below. */
.tab-pane{display:none}
.tab-pane.is-active{display:block}


/* ============================================================================
   7. CHAT PANE
   ========================================================================== */
.chat-pane.is-active{display:flex}
.chat-pane{
  flex-direction:column;
  height:calc(100vh - 220px);
  min-height:400px;
}
/* Capped at --layout-measure and left-aligned, so the reading column lines up
   with the tab bar while the header chrome still spans --layout-max. */
.chat-thread{
  flex:1;
  width:100%;
  max-width:var(--layout-measure);
  overflow-y:auto;
  padding:var(--space-5) 0;
  display:flex;
  flex-direction:column;
  gap:var(--space-5);
}

/* Messages */
.msg{display:flex;flex-direction:column;gap:var(--space-2);max-width:100%}
.msg--user{align-items:flex-end}
.msg--user .bubble{
  background:var(--color-accent);
  color:var(--color-ink-inverse);
  border-radius:var(--radius-bubble-user);
  max-width:75%;
}
.msg--assistant .bubble{
  background:var(--color-surface-raised);
  border:var(--border-width-thin) solid var(--color-border);
  border-radius:var(--radius-bubble-assistant);
  align-self:stretch;
  box-shadow:var(--shadow-xs);
}
.bubble{
  padding:var(--space-4) var(--space-5);
  font-size:var(--text-base);
  line-height:var(--leading-relaxed);
  word-break:break-word;
}

/* Markdown produced by renderMd() — these selectors are the render contract. */
.bubble h2{
  font-size:var(--text-xl);
  font-weight:var(--weight-semi);
  line-height:var(--leading-tight);
  margin:1em 0 .4em;
  padding-bottom:.25em;
  border-bottom:var(--border-width-thin) solid var(--color-border);
}
.bubble h3{
  font-size:var(--text-lg);
  font-weight:var(--weight-semi);
  line-height:var(--leading-tight);
  margin:.9em 0 .35em;
}
.bubble p{margin:.5em 0}
.bubble p:first-child{margin-top:0}
.bubble p:last-child{margin-bottom:0}
.bubble strong{font-weight:var(--weight-semi);color:var(--color-ink)}
.bubble em{color:var(--color-ink-soft)}
.bubble blockquote{
  border-left:3px solid var(--color-accent);
  border-radius:0 var(--radius-md) var(--radius-md) 0;
  background:var(--color-surface-quote);
  color:var(--color-ink-soft);
  padding:var(--space-2) var(--space-3);
  margin:var(--space-3) 0;
  font-style:italic;
}
.bubble code{
  background:var(--color-surface-code);
  border-radius:var(--radius-xs);
  padding:1px var(--space-1);
  font:var(--text-sm) var(--font-mono);
}
.bubble hr{border:none;border-top:var(--border-width-thin) solid var(--color-border);margin:var(--space-3) 0}
.bubble ul,.bubble ol{padding-left:1.5em;margin:.5em 0}
.bubble li{margin:.2em 0}


/* ============================================================================
   8. CITATIONS
   ========================================================================== */

/* Inline [PUH ...] reference button inside prose. */
.cite-ref{
  display:inline;
  background:var(--color-accent-quiet);
  color:var(--color-accent);
  border:var(--border-width-thin) solid var(--color-border-accent);
  border-radius:var(--radius-xs);
  padding:1px var(--space-1);
  font:var(--weight-semi) var(--text-xs) var(--font-mono);
  cursor:pointer;
  transition:background var(--duration-fast) var(--ease-out),
             border-color var(--duration-fast) var(--ease-out);
}
.cite-ref:hover{background:var(--blue-050);border-color:var(--blue-300)}

.citations{display:flex;flex-direction:column;gap:var(--space-3);margin-top:var(--space-3)}

.cite-card{
  background:var(--color-surface-sunken);
  border:var(--border-width-thin) solid var(--color-border);
  border-radius:var(--radius-lg);
  padding:var(--space-3) var(--space-4);
  font-size:var(--text-sm);
  transition:box-shadow var(--duration-base) var(--ease-out);
}
/* Fetched on demand when a [PUH ...] ref has no matching card. */
.cite-card--inline{
  display:inline-block;
  width:100%;
  margin-top:var(--space-2);
  background:var(--color-surface-raised);
  box-shadow:var(--shadow-md);
}
.cite-card.is-highlight{animation:pulse-border var(--duration-slow) var(--ease-out) 2}
@keyframes pulse-border{0%,100%{box-shadow:none}50%{box-shadow:0 0 0 3px var(--color-accent)}}

.cite-head{display:flex;gap:var(--space-2);align-items:baseline;flex-wrap:wrap;margin-bottom:var(--space-1)}
.cite-name{font-weight:var(--weight-semi);font-size:var(--text-md)}
.cite-meta{font-size:var(--text-xs);color:var(--color-ink-muted)}
.cite-meta code{font-size:var(--text-2xs);font-family:var(--font-mono)}
.cite-matter{font-size:var(--text-xs);color:var(--color-ink-muted);margin-bottom:var(--space-1);font-style:italic}
.cite-snippet{
  color:var(--color-ink-soft);
  font-size:var(--text-sm);
  line-height:var(--leading-normal);
  border-left:var(--border-width-thick) solid var(--color-border);
  padding-left:var(--space-3);
  margin:var(--space-2) 0;
}
.cite-expand{
  background:none;
  border:none;
  color:var(--color-accent);
  font-size:var(--text-xs);
  cursor:pointer;
  padding:var(--space-05) 0;
  margin-top:var(--space-1);
  text-decoration:underline;
}
.cite-expand:hover{color:var(--color-accent-hover)}
.cite-full{
  display:none;
  margin-top:var(--space-3);
  white-space:pre-wrap;
  font-size:var(--text-sm);
  line-height:var(--leading-normal);
  background:var(--color-surface-inset);
  border-radius:var(--radius-md);
  padding:var(--space-3);
  max-height:400px;
  overflow-y:auto;
}
.cite-card.is-expanded .cite-full{display:block}
.cite-full.is-open{display:block}


/* ============================================================================
   9. DISCLOSURE PANELS (tool activity, sources)
   Two collapsibles share one chrome: .tool-panel (what the model searched for)
   and .sources-panel (the speeches it found). Grouped selectors, not cloned
   rules — they must stay visually identical by construction.
   ========================================================================== */
.tool-panel,.sources-panel{
  background:var(--color-surface-sunken);
  border:var(--border-width-thin) solid var(--color-border);
  border-radius:var(--radius-lg);
  font-size:var(--text-xs);
  color:var(--color-ink-muted);
  overflow:hidden;
}
.tool-panel summary,.sources-panel summary{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  padding:var(--space-2) var(--space-3);
  cursor:pointer;
  list-style:none;
  user-select:none;
}
.tool-panel summary::-webkit-details-marker,
.sources-panel summary::-webkit-details-marker{display:none}
.tool-panel summary::before,
.sources-panel summary::before{
  content:"▶";
  font-size:9px;
  opacity:.5;
  transition:transform var(--duration-base) var(--ease-out);
}
.tool-panel[open] summary::before,
.sources-panel[open] summary::before{transform:rotate(90deg)}
.tool-label{flex:1}
.tool-count{opacity:.7;font-variant-numeric:tabular-nums}

/* Sources. Closed by default — see renderCitations() in index.html for why. */
.sources-panel__label{flex:1;font-weight:var(--weight-medium)}
.sources-panel__count{opacity:.7;font-variant-numeric:tabular-nums}
.sources-panel__note{
  padding:0 var(--space-3) var(--space-2);
  font-size:var(--text-xs);
  color:var(--color-ink-muted);
}
/* .citations carries a top margin for its old life as a bare sibling of the
   bubble; inside the panel the spacing belongs to the panel body. */
.sources-panel .citations{margin-top:0;padding:0 var(--space-3) var(--space-3)}
.tool-list{
  list-style:none;
  padding:0 var(--space-3) var(--space-2);
  display:flex;
  flex-direction:column;
  gap:var(--space-1);
}
.tool-list li{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  padding:var(--space-1) 0;
  border-top:var(--border-width-thin) solid var(--color-border);
}
.tool-list li::before{
  content:"";
  width:6px;height:6px;
  border-radius:var(--radius-full);
  background:var(--color-accent);
  opacity:.5;
  flex-shrink:0;
}
.tool-list li:first-child{border-top:none}


/* ============================================================================
   10. PARTY BADGES
   Driven by data-party so unknown codes degrade to the neutral fallback
   instead of disappearing. The database uses `r` for RKP, not `rkp`, and
   ~25k rows have no party at all — both are handled here.
   All foreground/background pairs are >= 4.5:1 (WCAG AA).
   ========================================================================== */
.badge{
  font-size:var(--text-2xs);
  padding:var(--space-05) var(--space-2);
  border-radius:var(--radius-full);
  white-space:nowrap;
  /* Badges sit in flex rows whose default align-items is stretch. A taller
     sibling (an .mp-link with touch padding) would stretch the pill until its
     height matched its width and --radius-full turned it into a circle. */
  align-self:center;
  flex-shrink:0;
  background:var(--party-bg,var(--color-surface-code));
  color:var(--party-fg,var(--slate-700));
}
:is(.badge,.mp-avatar)[data-party="sd"]   {--party-fg:#9d2b3a;--party-bg:#f5dfe3}
:is(.badge,.mp-avatar)[data-party="kok"]  {--party-fg:#1d5c9e;--party-bg:#dbe8f5}
:is(.badge,.mp-avatar)[data-party="kesk"] {--party-fg:#2e773c;--party-bg:#dff0e2}
:is(.badge,.mp-avatar)[data-party="ps"]   {--party-fg:#4a5a78;--party-bg:#dfe7ef}
:is(.badge,.mp-avatar)[data-party="vas"]  {--party-fg:#b03030;--party-bg:#f6e2df}
:is(.badge,.mp-avatar)[data-party="vihr"] {--party-fg:#267941;--party-bg:#e0f2e4}
:is(.badge,.mp-avatar)[data-party="kd"]   {--party-fg:#5a4a9e;--party-bg:#e8e4f2}
:is(.badge,.mp-avatar)[data-party="r"],
:is(.badge,.mp-avatar)[data-party="rkp"]  {--party-fg:#7e641c;--party-bg:#f0e9d8}
/* Smaller / historical groups present in the corpus. */
:is(.badge,.mp-avatar)[data-party="skl"]  {--party-fg:#5a4a9e;--party-bg:#e8e4f2}
:is(.badge,.mp-avatar)[data-party="sin"]  {--party-fg:#1c6273;--party-bg:#d9eef2}
:is(.badge,.mp-avatar)[data-party="vr"]   {--party-fg:#a03a52;--party-bg:#f6e0e6}
:is(.badge,.mp-avatar)[data-party="liik"] {--party-fg:#9a4c22;--party-bg:#f7e3d8}


/* ----------------------------------------------------------------------------
   10b. STANCE PILL — hallitus / oppositio
   A property of the *speech*, not the speaker: the same MP is government in
   one year and opposition in the next, so this never appears on a speaker row
   or in the MP profile header. Driven by data-stance with no fallback rule —
   an unknown value renders as the bare neutral pill rather than a wrong side.
   Contrast: gov #ffffff on #3c4656 = 9.2:1; opp #3c4656 on the card surfaces
   (#ffffff / #faf9f5 / #eeebe1) = 9.2 / 8.7 / 7.6:1. All clear AA.
   -------------------------------------------------------------------------- */
.stance{
  font-size:var(--text-2xs);
  font-weight:var(--weight-medium);
  padding:var(--space-05) var(--space-2);
  border-radius:var(--radius-full);
  white-space:nowrap;
  /* Same stretch trap as .badge — see the note there. */
  align-self:center;
  flex-shrink:0;
  /* The border is declared on every variant so the two pills are the same
     height and the row does not jog as stance changes down a list. */
  border:var(--border-width-thin) solid transparent;
  background:transparent;
  color:var(--color-ink-muted);
}
.stance[data-stance="hallitus"]{
  background:var(--color-stance-gov-bg);
  border-color:var(--color-stance-gov-bg);
  color:var(--color-stance-gov-fg);
}
.stance[data-stance="oppositio"]{
  background:transparent;
  border-color:var(--color-stance-opp-border);
  color:var(--color-stance-opp-fg);
}


/* ----------------------------------------------------------------------------
   10c. OUTCOME PILL — what happened to the matter under debate
   A property of the *matter*, not the speech and not the speaker: every speech
   on HE 19/2020 vp carries the same one. Sits on its own line under the matter
   title rather than in the header row, which already holds the party badge and
   the stance pill.
   Driven by data-outcome with no fallback rule, same as .stance — an outcome
   code this stylesheet has not seen renders neutral rather than guessing a
   colour, which matters because matter_scraper.py leaves an unrecognised
   decision NULL on purpose.
   -------------------------------------------------------------------------- */
.outcome{
  display:inline-flex;
  align-items:baseline;
  gap:var(--space-1);
  font-size:var(--text-2xs);
  font-weight:var(--weight-medium);
  padding:var(--space-05) var(--space-2);
  border-radius:var(--radius-full);
  white-space:nowrap;
  max-width:100%;
  overflow:hidden;
  text-overflow:ellipsis;
  /* Outline in every variant — see the token note for why this one is never
     filled. The border is always declared so the pill height cannot jog. */
  border:var(--border-width-thin) solid currentColor;
  background:transparent;
  color:var(--color-outcome-neutral);
}
.outcome[data-outcome="approved"],
.outcome[data-outcome="approved_modified"],
.outcome[data-outcome="approved_urgent"]{color:var(--color-outcome-pass)}
.outcome[data-outcome="rejected"]{color:var(--color-outcome-fail)}
/* The statute reference is the citable half — "911/2020" is what proves the
   bill became law, so it must not be dimmed into decoration. */
.outcome__statute{font-weight:var(--weight-normal);opacity:0.85}


/* ============================================================================
   11. FEEDBACK — typing indicator, spinner
   ========================================================================== */
.typing{display:flex;gap:var(--space-1);align-items:center;padding:var(--space-3) var(--space-5)}
.typing__dot{
  width:7px;height:7px;
  border-radius:var(--radius-full);
  background:var(--color-ink-muted);
  animation:bounce .9s infinite;
}
.typing__dot:nth-child(2){animation-delay:.2s}
.typing__dot:nth-child(3){animation-delay:.4s}
@keyframes bounce{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-6px)}}

.spinner{
  display:inline-block;
  width:12px;height:12px;
  border:var(--border-width-thick) solid var(--color-border-strong);
  border-top-color:var(--color-accent);
  border-radius:var(--radius-full);
  animation:spin .7s linear infinite;
  vertical-align:-2px;
  flex-shrink:0;
}
@keyframes spin{to{transform:rotate(360deg)}}


/* ============================================================================
   12. FORM CONTROLS & BUTTONS
   ========================================================================== */
.field{
  padding:var(--space-2) var(--space-3);
  border:var(--border-width-thin) solid var(--color-border);
  border-radius:var(--radius-md);
  background:var(--color-surface-raised);
  color:var(--color-ink);
  font:var(--text-sm) var(--font-sans);
  transition:border-color var(--duration-fast) var(--ease-out);
}
.field:hover{border-color:var(--color-border-strong)}

.btn{
  border:var(--border-width-thin) solid transparent;
  border-radius:var(--radius-md);
  font:var(--weight-medium) var(--text-md) var(--font-sans);
  cursor:pointer;
  white-space:nowrap;
  transition:background var(--duration-fast) var(--ease-out),
             border-color var(--duration-fast) var(--ease-out),
             color var(--duration-fast) var(--ease-out);
}
.btn:disabled{opacity:.45;cursor:default}
.btn--primary{
  padding:var(--space-3) var(--space-6);
  background:var(--color-accent);
  color:var(--color-ink-inverse);
}
.btn--primary:not(:disabled):hover{background:var(--color-accent-hover)}
.btn--ghost{
  padding:var(--space-2) var(--space-3);
  background:var(--color-surface-raised);
  border-color:var(--color-border);
  color:var(--color-ink);
  font-size:var(--text-sm);
}
.btn--ghost:not(:disabled):hover{border-color:var(--color-accent);color:var(--color-accent)}

/* Icon+label buttons collapse to icon-only under sm=480px (section 17).
   The accessible name is aria-label on the button itself, because
   .btn__label leaves the render tree at that width and would otherwise
   take the accessible name with it. */
.btn--icon-label .btn__icon{margin-inline-end:var(--space-2)}


/* ============================================================================
   13. COMPOSER (chat input row)
   ========================================================================== */
/* The input row and the review block are siblings inside .composer, swapped
   with .is-hidden. Replacing the composer's children instead would detach
   .scroll-latest from its positioning context and strand the DOM handles
   index.html captures once at load — both fail silently rather than throwing. */
.composer{
  position:relative;   /* positioning context for .scroll-latest */
  flex-shrink:0;
  width:100%;
  max-width:var(--layout-measure);
  padding-top:var(--space-3);
  border-top:var(--border-width-thin) solid var(--color-border);
}

/* Two native range inputs share a rail. Thumb-only pointer targets let both
   handles work; native keyboard controls and focus rings remain available. */
.year-range{
  min-width:0;
  border:0;
  padding:0;
  margin:0 0 var(--space-3);
}
.year-range__legend{
  color:var(--color-ink-muted);
  font-size:var(--text-xs);
  font-weight:var(--weight-medium);
}
.year-range__head{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--space-3);
}
.year-range__value{
  color:var(--color-ink);
  font-size:var(--text-md);
  font-weight:var(--weight-semi);
  font-variant-numeric:tabular-nums;
}
.year-range__reset{
  min-height:44px;
  padding:0 var(--space-1);
  border:0;
  background:transparent;
  color:var(--color-accent);
  font:var(--text-xs) var(--font-sans);
  cursor:pointer;
}
.year-range__reset:not(:disabled):hover{color:var(--color-accent-hover);text-decoration:underline}
.year-range__reset:disabled{color:var(--color-ink-muted);cursor:default}
.year-range__sliders{position:relative;height:44px}
.year-range__track{
  position:absolute;
  left:12px;
  width:calc(100% - 24px);
  height:44px;
  overflow:visible;
  pointer-events:none;
}
.year-range__rail,.year-range__fill{stroke-width:4;stroke-linecap:round}
.year-range__rail{stroke:var(--color-border-strong)}
.year-range__fill{stroke:var(--color-accent)}
.year-range__input{
  position:absolute;
  inset:0;
  width:100%;
  height:44px;
  margin:0;
  appearance:none;
  -webkit-appearance:none;
  background:transparent;
  pointer-events:none;
}
.year-range__input.is-on-top{z-index:var(--z-sticky)}
.year-range__input::-webkit-slider-runnable-track{height:4px;background:transparent}
.year-range__input::-moz-range-track{height:4px;background:transparent}
.year-range__input::-webkit-slider-thumb{
  width:24px;
  height:24px;
  margin-top:calc(-1 * var(--space-2) - var(--space-05));
  -webkit-appearance:none;
  border:var(--border-width-thick) solid var(--color-accent);
  border-radius:var(--radius-full);
  background:var(--color-surface-raised);
  box-shadow:var(--shadow-xs);
  cursor:ew-resize;
  pointer-events:auto;
}
.year-range__input::-moz-range-thumb{
  width:20px;
  height:20px;
  border:var(--border-width-thick) solid var(--color-accent);
  border-radius:var(--radius-full);
  background:var(--color-surface-raised);
  box-shadow:var(--shadow-xs);
  cursor:ew-resize;
  pointer-events:auto;
}
.year-range:disabled .year-range__sliders{opacity:.5}
.year-range:disabled .year-range__input::-webkit-slider-thumb{cursor:default}
.year-range:disabled .year-range__input::-moz-range-thumb{cursor:default}
.year-range__ends{
  display:flex;
  justify-content:space-between;
  color:var(--color-ink-muted);
  font-size:var(--text-2xs);
  font-variant-numeric:tabular-nums;
}
.msg__scope{
  color:var(--color-ink-muted);
  font-size:var(--text-xs);
}
/* Toggle is hidden above sm=480px, where .year-range__body{display:contents}
   dissolves the wrapper and the sliders/ends stay direct fieldset children —
   same idiom as .toolbar__filters. Below sm the toggle appears and the body
   becomes a real collapsible block; see section 17. */
.year-range__toggle{display:none}
.year-range__body{display:contents}

.composer__row{
  display:flex;
  gap:var(--space-3);
  width:100%;
}
.composer__status{
  padding-bottom:var(--space-2);
  font:var(--text-xs) var(--font-sans);
}
.composer__input{
  flex:1;
  padding:var(--space-3);
  border:var(--border-width-thin) solid var(--color-border);
  border-radius:var(--radius-md);
  background:var(--color-surface-raised);
  color:var(--color-ink);
  font:var(--text-md)/var(--leading-normal) var(--font-sans);
  resize:none;
  min-height:44px;
  /* JS caps growth at this value — see design.md "Deliberate deviations". */
  max-height:160px;
  overflow-y:auto;
}
.composer__input:focus{
  outline:var(--border-width-thick) solid var(--color-accent-quiet);
  border-color:var(--color-accent);
}
.composer__row .btn{align-self:flex-end}

/* Beta review block. Asked in the composer rather than under the message
   because that is where the reader's attention and cursor already are once an
   answer lands. The star row reuses the .sort-tab segmented idiom; buttons are
   real <button>s so the global :focus-visible ring applies. */
.composer__review{
  display:flex;
  flex-direction:column;
  gap:var(--space-2);
}
.composer__review-label{
  font:var(--weight-medium) var(--text-sm) var(--font-sans);
  color:var(--color-ink-soft);
}
.stars{display:flex;gap:var(--space-1)}
.star{
  flex:1;
  text-align:center;
  padding:var(--space-2) 0;
  border:var(--border-width-thin) solid var(--color-border);
  border-radius:var(--radius-sm);
  background:var(--color-surface-inset);
  color:var(--color-ink-muted);
  font:var(--text-sm) var(--font-sans);
  cursor:pointer;
  transition:background var(--duration-fast) var(--ease-out),
             color var(--duration-fast) var(--ease-out);
}
.star:hover{color:var(--color-ink)}
.star.is-active{
  background:var(--color-accent-quiet);
  color:var(--color-accent);
  border-color:var(--color-border-accent);
  font-weight:var(--weight-semi);
}
.composer__review-text{
  width:100%;
  resize:none;
  min-height:44px;
  font:var(--text-sm)/var(--leading-normal) var(--font-sans);
}
.composer__review-actions{
  display:flex;
  gap:var(--space-3);
  align-items:center;
  /* Buttons + hint need ~405px on one line; a 400px phone offers ~360. Wrap
     lets the hint drop below the buttons instead of overflowing the composer. */
  flex-wrap:wrap;
}
.composer__review-actions .btn--primary{padding:var(--space-2) var(--space-5)}
.composer__review-hint{
  font:var(--text-xs) var(--font-sans);
  color:var(--color-ink-muted);
}

/* Shown only while an answer streams and the reader has scrolled away from the
   bottom. The thread no longer force-pins itself, so this is the only signal
   that the answer has moved on. Anchored to the composer, not the pane, so it
   tracks the divider as the textarea auto-grows. */
.scroll-latest{
  position:absolute;
  left:50%;
  bottom:calc(100% + var(--space-2));
  transform:translateX(-50%);
  z-index:var(--z-sticky);
  padding:var(--space-2) var(--space-4);
  border:var(--border-width-thin) solid var(--color-border-accent);
  border-radius:var(--radius-full);
  background:var(--color-accent);
  color:var(--color-ink-inverse);
  font-family:var(--font-sans);
  font-size:var(--text-xs);
  font-weight:var(--weight-medium);
  white-space:nowrap;
  cursor:pointer;
  box-shadow:var(--shadow-md);
  transition:background var(--duration-fast) var(--ease-out);
}
.scroll-latest:hover{background:var(--color-accent-hover)}


/* ============================================================================
   14. BROWSE PANE
   ========================================================================== */
.browse-grid{
  display:grid;
  grid-template-columns:280px 1fr;
  gap:var(--space-4);
  padding:var(--space-5) 0 var(--space-10);
}

.panel{
  background:var(--color-surface-raised);
  border:var(--border-width-thin) solid var(--color-border);
  border-radius:var(--radius-lg);
  box-shadow:var(--shadow-xs);
}
.panel__head{padding:var(--space-3);border-bottom:var(--border-width-thin) solid var(--color-border)}
.panel__head .field{width:100%}

.speakers-panel{align-self:start;max-height:75vh;display:flex;flex-direction:column;overflow:hidden}
.speaker-list{overflow-y:auto;padding:var(--space-1)}

.sort-tabs{display:flex;gap:var(--space-1);margin-top:var(--space-2)}
.sort-tab{
  flex:1;
  text-align:center;
  padding:var(--space-1) 0;
  border:var(--border-width-thin) solid var(--color-border);
  border-radius:var(--radius-sm);
  background:var(--color-surface-inset);
  color:var(--color-ink-muted);
  font:var(--text-xs) var(--font-sans);
  cursor:pointer;
  transition:background var(--duration-fast) var(--ease-out),
             color var(--duration-fast) var(--ease-out);
}
.sort-tab:hover{color:var(--color-ink)}
.sort-tab.is-active{
  background:var(--color-accent-quiet);
  color:var(--color-accent);
  border-color:var(--color-border-accent);
  font-weight:var(--weight-semi);
}

.speaker-item{
  display:flex;
  justify-content:space-between;
  align-items:center;
  gap:var(--space-2);
  padding:var(--space-2);
  border-radius:var(--radius-md);
  cursor:pointer;
  transition:background var(--duration-fast) var(--ease-out);
}
.speaker-item:hover{background:var(--color-surface-sunken)}
.speaker-item.is-active{background:var(--color-accent-quiet);outline:var(--border-width-thin) solid var(--color-border-accent)}
.speaker-item__main{min-width:0}
.speaker-item__name{font-weight:var(--weight-semi);font-size:var(--text-sm)}
/* The row already filters, so the profile needs its own target rather than a
   second tap action hidden inside the row's label. */
.speaker-item__info{
  flex-shrink:0;
  display:inline-flex;
  align-items:center;
  background:none;
  border:none;
  cursor:pointer;
  padding:var(--space-1);
  line-height:1;
  font-size:var(--text-md);
  color:var(--color-ink-muted);
  border-radius:var(--radius-full);
  text-decoration:none;
}
.speaker-item__info:hover{color:var(--color-accent);background:var(--color-accent-quiet)}
/* The badge moved onto this line when the row gained a portrait: name and
   badge were competing for one line, and at 40px the avatar tipped every
   two-word name onto a second row. Stacked, the name gets the full width. */
.speaker-item__meta{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  margin-top:var(--space-05);
}
.speaker-item__years{font-size:var(--text-2xs);color:var(--color-ink-muted)}
.speaker-item__count{
  margin-left:auto;
  font-size:var(--text-2xs);
  color:var(--color-ink-muted);
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
}

.toolbar{
  display:flex;
  gap:var(--space-2);
  flex-wrap:wrap;
  align-items:center;
  padding:var(--space-3) var(--space-4);
  border-bottom:var(--border-width-thin) solid var(--color-border);
}
.toolbar__title{font-weight:var(--weight-semi);font-size:var(--text-base)}
.toolbar .field[type=text]{flex:1;min-width:140px}
.toolbar__count{margin-left:auto;font-size:var(--text-xs);color:var(--color-ink-muted)}
/* display:contents dissolves the wrapper so the fields stay direct flex items
   of .toolbar — desktop layout is identical to the previous flat markup. The
   wrapper only becomes a real box in the narrow rule in section 17. */
.toolbar__filters{display:contents}
.toolbar__toggle{display:none}

.speech-list{padding:var(--space-2) var(--space-4)}
.speech-item{
  padding:var(--space-3) var(--space-1);
  border-bottom:var(--border-width-thin) solid var(--color-border);
  cursor:pointer;
}
.speech-item:last-child{border-bottom:none}
.speech-item__title{
  font-weight:var(--weight-semi);
  font-size:var(--text-sm);
  transition:color var(--duration-fast) var(--ease-out);
}
.speech-item:hover .speech-item__title{color:var(--color-accent)}
.speech-item__meta{
  display:flex;
  gap:var(--space-2);
  flex-wrap:wrap;
  margin-top:var(--space-1);
  font-size:var(--text-xs);
  color:var(--color-ink-muted);
}
/* Named hook for the speaker slot; the name inside is an .mp-link. */
.speech-item__speaker{color:var(--color-ink-soft)}
.speech-item__snippet{font-size:var(--text-sm);color:var(--color-ink-soft);margin-top:var(--space-1)}
.speech-item__full{
  display:none;
  margin-top:var(--space-2);
  padding:var(--space-3);
  background:var(--color-surface-sunken);
  border:var(--border-width-thin) solid var(--color-border);
  border-radius:var(--radius-md);
  white-space:pre-wrap;
  font-size:var(--text-sm);
  line-height:var(--leading-normal);
}
.speech-item.is-open .speech-item__full{display:block}
.speech-item.is-open .speech-item__snippet{display:none}

.pager{
  display:flex;
  gap:var(--space-2);
  justify-content:center;
  align-items:center;
  padding:var(--space-3);
  border-top:var(--border-width-thin) solid var(--color-border);
}
.pager__info{font-size:var(--text-sm);color:var(--color-ink-muted);font-variant-numeric:tabular-nums}

.empty-state{padding:var(--space-8);text-align:center;color:var(--color-ink-muted)}


/* ============================================================================
   15. MP PROFILE DRAWER
   Slides in from the right when an MP's name is clicked. Below md it becomes a
   full-width sheet. `hidden` is removed first, then `.is-open` on the next
   frame, so the transition actually runs.
   ========================================================================== */
.mp-scrim{
  position:fixed;
  inset:0;
  background:var(--color-ink);
  opacity:0;
  transition:opacity var(--duration-base) var(--ease-out);
  z-index:var(--z-overlay);
}
.mp-scrim.is-open{opacity:.32}

.mp-drawer{
  position:fixed;
  top:0;
  right:0;
  bottom:0;
  width:min(24rem,100vw);
  display:flex;
  flex-direction:column;
  background:var(--color-surface-raised);
  border-left:var(--border-width-thin) solid var(--color-border);
  box-shadow:var(--shadow-lg);
  transform:translateX(100%);
  transition:transform var(--duration-base) var(--ease-out);
  z-index:var(--z-modal);
}
.mp-drawer.is-open{transform:translateX(0)}

.mp-drawer__head{
  display:flex;
  align-items:flex-start;
  gap:var(--space-3);
  padding:var(--space-5);
  border-bottom:var(--border-width-thin) solid var(--color-border);
}
/* Portraits come from Wikimedia Commons and cover roughly two thirds of the
   MPs in the corpus, so the monogram is an ordinary state, not an error: it
   borrows the party palette above so a photo-less row still reads as someone. */
.mp-avatar{
  flex-shrink:0;
  width:88px;
  height:88px;
  border-radius:var(--radius-full);
  overflow:hidden;
  background:var(--color-surface-sunken);
  display:flex;
  align-items:center;
  justify-content:center;
}
.mp-avatar img{
  width:100%;
  height:100%;
  /* Commons photos are arbitrary crops — cover keeps the circle filled, and
     top-biased framing keeps the head in frame on full-body shots. */
  object-fit:cover;
  object-position:50% 20%;
  display:block;
}
.mp-avatar--mono{
  font-size:var(--text-lg);
  font-weight:var(--weight-semi);
  letter-spacing:0.02em;
  background:var(--party-bg,var(--color-surface-code));
  color:var(--party-fg,var(--slate-700));
}
.mp-avatar--sm{width:40px;height:40px}
.mp-avatar--sm.mp-avatar--mono{font-size:var(--text-xs)}
/* The name block must be able to shrink, or a long name pushes the close
   button off the drawer edge now that the portrait takes fixed width. */
.mp-drawer__headtext{min-width:0}
/* CC BY-SA requires the credit next to the image, so it is part of the header
   rather than a page footer. */
.mp-credit{
  margin-top:var(--space-1);
  font-size:var(--text-2xs);
  color:var(--color-ink-muted);
}
.mp-credit:empty{display:none}
.mp-credit a{color:inherit;text-decoration:underline}
.mp-credit a:hover{color:var(--color-accent)}

.mp-drawer__name{
  font-size:var(--text-lg);
  font-weight:var(--weight-semi);
  color:var(--color-ink);
  margin:0;
}
.mp-drawer__sub{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  flex-wrap:wrap;
  margin-top:var(--space-1);
  font-size:var(--text-xs);
  color:var(--color-ink-muted);
}
.mp-drawer__close{
  margin-left:auto;
  flex-shrink:0;
  background:none;
  border:none;
  cursor:pointer;
  font-size:var(--text-md);
  line-height:1;
  padding:var(--space-1);
  color:var(--color-ink-muted);
  border-radius:var(--radius-sm);
}
.mp-drawer__close:hover{color:var(--color-ink);background:var(--color-surface-sunken)}

.mp-drawer__body{
  flex:1;
  overflow-y:auto;
  padding:var(--space-5);
}

.mp-section{margin-bottom:var(--space-5)}
.mp-section:last-child{margin-bottom:0}
.mp-section__title{
  font-size:var(--text-2xs);
  font-weight:var(--weight-semi);
  letter-spacing:.06em;
  text-transform:uppercase;
  color:var(--color-ink-muted);
  margin-bottom:var(--space-2);
}
.mp-row{
  display:flex;
  gap:var(--space-3);
  justify-content:space-between;
  align-items:baseline;
  padding:var(--space-2) 0;
  border-bottom:var(--border-width-thin) solid var(--color-border);
  font-size:var(--text-sm);
}
.mp-row:last-child{border-bottom:none}
.mp-section__action{margin-top:var(--space-3);width:100%}
.mp-row__label{color:var(--color-ink-soft)}
.mp-row__value{
  color:var(--color-ink-muted);
  font-size:var(--text-xs);
  white-space:nowrap;
  font-variant-numeric:tabular-nums;
}

/* One line per election the MP stood in. Three columns rather than .mp-row's
   two, because the year is a spine the eye scans down and must not be mixed
   into the district label. */
.mp-election{
  display:grid;
  grid-template-columns:auto 1fr auto;
  gap:var(--space-3);
  align-items:baseline;
  padding:var(--space-2) 0;
  border-bottom:var(--border-width-thin) solid var(--color-border);
  font-size:var(--text-sm);
}
.mp-election:last-child{border-bottom:none}
.mp-election__year{
  font-weight:var(--weight-semi);
  font-variant-numeric:tabular-nums;
  color:var(--color-ink);
}
.mp-election__where{color:var(--color-ink-soft);min-width:0}
.mp-election__votes{
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
  color:var(--color-ink);
}
.mp-election__votes span{font-size:var(--text-2xs);color:var(--color-ink-muted)}
/* An unsuccessful run belongs in the record but is not the headline: muting it
   lets the years they won read as the spine of the list. */
.mp-election--lost .mp-election__year,
.mp-election--lost .mp-election__votes{
  color:var(--color-ink-muted);
  font-weight:var(--weight-normal);
}
.mp-election__note{color:var(--color-ink-muted);font-size:var(--text-2xs)}

/* Grid rather than wrapping flex: an MP with an election result has four tiles,
   and under flex the one pushed onto the second row stretched the full width of
   the drawer instead of keeping its column. */
.mp-facts{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(6rem,1fr));
  gap:var(--space-2);
}
.mp-fact{
  padding:var(--space-3);
  background:var(--color-surface-sunken);
  border-radius:var(--radius-md);
  text-align:center;
}
.mp-fact b{
  display:block;
  font-size:var(--text-md);
  font-weight:var(--weight-semi);
  color:var(--color-ink);
  font-variant-numeric:tabular-nums;
}
.mp-fact span{font-size:var(--text-2xs);color:var(--color-ink-muted)}

/* The name itself is the affordance, everywhere it appears. */
.mp-link{
  background:none;
  border:none;
  padding:0;
  font:inherit;
  color:inherit;
  cursor:pointer;
  /* text-decoration, not border-bottom: the mobile rule adds padding-block for
     hit area, which would drag a border away from the text but leaves an
     underline attached to the glyphs. */
  text-decoration:underline;
  text-decoration-style:dotted;
  text-decoration-color:var(--color-border-strong);
  text-underline-offset:3px;
}
.mp-link:hover{color:var(--color-accent);text-decoration-color:var(--color-accent)}

@media (max-width:800px){
  .mp-drawer{width:100vw;border-left:none}
}


/* ============================================================================
   16. FOOTER & USAGE BAR
   ========================================================================== */
.usage-bar{
  font-size:var(--text-2xs);
  color:var(--color-ink-muted);
  text-align:right;
  padding:var(--space-05) var(--space-1);
  font-variant-numeric:tabular-nums;
  opacity:.8;
}
.app-footer{
  padding:var(--space-5);
  margin-top:auto;
  text-align:center;
  color:var(--color-ink-muted);
  font-size:var(--text-xs);
}


/* ============================================================================
   17. RESPONSIVE
   Breakpoint md = 800px (mirrors --breakpoint-md; @media cannot read a var).
   ========================================================================== */
@media (max-width:800px){
  .browse-grid{grid-template-columns:1fr}
  .app-header{padding:var(--space-4) 0 var(--space-3)}
  .app-header__title{font-size:var(--text-xl)}
  .app-header__subtitle{font-size:var(--text-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  /* Chat-only on mobile — Selaa (browse) has no phone-sized layout of its own,
     and the tab bar was the single biggest chunk of chrome above the fold.
     #chatPane is the default-active pane, so this never traps a reader on
     Browse; there is simply no way to navigate there from a phone. */
  .tabs{display:none}
  .msg--user .bubble{max-width:90%}
  .bubble{padding:var(--space-3) var(--space-4)}
  .wrap{padding:0 var(--space-4)}

  /* App-shell layout. `calc(100vh - 220px)` is tuned for the desktop header;
     on a phone the header is a different height and the composer — the app's
     primary input — ended up below the fold.
     body must have a DEFINITE height here, not min-height: with min-height the
     container grows to fit content, so `flex:1` caps nothing and a long answer
     stretched the chat pane to several thousand px. Scrolling moves inside
     .chat-thread (chat) and .wrap--main (browse) instead of on the page. */
  body{height:100dvh;overflow:hidden}
  .wrap--main{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto}
  .chat-pane{height:auto;min-height:0}
  .chat-pane.is-active{flex:1;min-height:0}
  .chat-thread{padding:var(--space-4) 0}
  .app-footer{padding:var(--space-3) var(--space-5)}

  /* The speaker list is a sidebar on desktop; stacked on a phone it pushed the
     speeches out of reach, so cap it and let the list scroll inside itself. */
  .speakers-panel{max-height:42vh}
  .toolbar{flex-wrap:wrap}
  .toolbar__count{margin-left:0;width:100%}

  /* Touch targets. 44px is the Apple/Android floor; several controls sat at
     22–33px. */
  .tab-btn{padding:var(--space-3) var(--space-4);min-height:44px}
  .scroll-latest{min-height:44px;padding:var(--space-2) var(--space-5)}
  .btn{min-height:44px}
  .field{min-height:44px}
  .sort-tab{min-height:44px}
  .star{min-height:44px}
  .speaker-item{padding:var(--space-3)}
  /* .mp-link sits inline inside a row that has its own tap action, so it can't
     be 44px tall without breaking the line box. Padding buys hit area without
     changing where the text sits; the row keeps its own line-height so the
     taller link does not push its siblings around. */
  .mp-link{padding-block:var(--space-2)}
  .speech-item__meta{align-items:center;row-gap:var(--space-1)}
  .mp-drawer__close{min-width:44px;min-height:44px}
  .mp-drawer__head .mp-avatar{width:72px;height:72px}
  .speaker-item__info{min-width:44px;min-height:44px;justify-content:center}
  .cite-expand{min-height:44px}

  /* iOS Safari zooms the whole page when a focused control is under 16px.
     --text-base is 15px and the controls were 13–14px, so set 1rem here
     explicitly rather than inflating the type scale for everyone. */
  .field,.composer__input,.btn{font-size:1rem}
}


/* Narrow phones. Breakpoint sm = 480px (mirrors --breakpoint-sm).
   Below this the speech toolbar stacked title/year/search/count onto four
   lines — ~180px at 320px wide, half the visible list — so the two filters
   collapse behind a disclosure and the header stays one line. */
@media (max-width:480px){
  /* A selected speaker's name can be long; truncate rather than let it wrap
     and then get clipped by the toggle sitting next to it. */
  .toolbar__title{
    flex:1;
    min-width:0;
    white-space:nowrap;
    overflow:hidden;
    text-overflow:ellipsis;
  }
  .toolbar__count{margin-left:0;width:auto;white-space:nowrap}
  .toolbar__toggle{
    display:inline-flex;
    align-items:center;
    min-height:44px;
    padding:var(--space-1) var(--space-3);
    border:var(--border-width-thin) solid var(--color-border);
    border-radius:var(--radius-sm);
    background:var(--color-surface-inset);
    color:var(--color-ink-soft);
    font:var(--weight-medium) var(--text-xs) var(--font-sans);
    cursor:pointer;
  }
  /* Open, or closed-but-filtering, both need to read as "active" — otherwise a
     collapsed filter silently narrows the results with nothing on screen. */
  .toolbar__toggle[aria-expanded="true"],
  .toolbar__toggle.is-filtered{
    background:var(--color-accent-quiet);
    border-color:var(--color-border-accent);
    color:var(--color-accent);
  }
  .toolbar__filters{display:none}
  .toolbar__filters.is-open{
    display:flex;
    flex-direction:column;
    gap:var(--space-2);
    width:100%;
  }
  .toolbar .field[type=text]{min-width:0}

  /* Icon-only buttons — see .btn--icon-label base rule in section 12. Only
     applied to #newChatBtn and #sendBtn; pager and .toolbar__toggle stay
     full-text at every width. */
  .btn--icon-label{display:inline-flex;align-items:center;justify-content:center;width:44px;padding:0}
  .btn--icon-label .btn__label{display:none}
  .btn--icon-label .btn__icon{margin-inline-end:0;font-size:var(--text-xl)}

  /* Year-range disclosure — second reuse of the .toolbar__filters
     display:contents idiom above. #chatYearLabel stays in .year-range__head,
     outside the collapsible body, so the current selection is always visible
     even while collapsed — no .is-filtered-style cue is needed here. */
  .year-range__value{flex:1;min-width:0}
  .year-range__toggle{
    display:inline-flex;
    align-items:center;
    justify-content:center;
    min-width:44px;
    min-height:44px;
    border:var(--border-width-thin) solid var(--color-border);
    border-radius:var(--radius-sm);
    background:var(--color-surface-inset);
    color:var(--color-ink-soft);
    font-size:var(--text-2xl);
    line-height:1;
    cursor:pointer;
    transition:transform var(--duration-fast) var(--ease-out);
  }
  .year-range__toggle[aria-expanded="true"]{transform:rotate(180deg)}
  .year-range__body{display:none}
  .year-range__body.is-open{display:block}
}


/* ============================================================================
   18. MOTION PREFERENCES
   Zeroes every duration token, so anything built on the tokens honours this
   automatically. Keyframe animations are stopped explicitly.
   ========================================================================== */
@media (prefers-reduced-motion:reduce){
  :root{
    --duration-fast:0ms;
    --duration-base:0ms;
    --duration-slow:0ms;
  }
  *,*::before,*::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
    scroll-behavior:auto !important;
  }
}
