/* ==========================================================================
   BillTracking — feed pages, cards, account.  Loaded AFTER style.css.

   This file is the web half of ONE design. Every number in the card section
   is the app's own number, lifted from `app/src/components/post-card.tsx`,
   `vote-chips.tsx` and `collapsible-body.tsx` and governed by
   `app/COMPONENT-STYLE.md` — the card must look like the app's card, because
   it renders the same record. When the app changes, this changes with it.

   Structure:
     1. Scope tokens (incl. the AA fix for --ink-faint)
     2. Skip link + focus
     3. Feed page shell (head, freshness, feed, status, show-more)
     4. The card
     5. Account / callback / resolver pages
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Scope tokens
   -------------------------------------------------------------------------- */

/* --ink-faint (#6B7C90) fails WCAG AA as text on every background this site
   puts it on: 4.28 on white, 4.13 on paper, 3.87 on wash, 3.78 on the EU wash
   (UNIFIED-WEB-SCOPE-2026-08-17, accessibility defect 1). None of the places
   it is used are large text — 10 to 12.5 px — so AA-large does not rescue it.
   Same hue family, three steps darker: 5.21 / 5.03 / 4.72 / 4.61. Used for
   EVERY faint text in this file; --ink-faint itself is left alone so the rest
   of the site is untouched. */
.feed-page,
.account-page,
#bt-callback,
#bt-resolver {
  --ink-faint-aa: #5E6E82;
  --card-radius: 12px;
  --feed-measure: 640px;   /* ≈ ⅓ of 1920 — the site's article measure */
}

/* style.css's `main section { padding: 3.25rem 0 }` is right for the prose
   pages, where a <section> IS a band of the page. Here a <section> is a
   region — the feed, an account block — and that padding opens a 52 px hole
   above the first card. Neutralised in scope only; style.css is untouched.
   ⚠️ THE FENCE (found 2026-08-22, via the signed-in preview): at (0,1,1) this
   reset also BEATS one-class component rules like `.acct-card` (0,1,0), so a
   card-shaped <section> on these pages renders UNPADDED — it cost the delete
   card its padding for three days. Card-shaped blocks here are DIVs
   (`deleteCardIdleHtml` carries the same note); if a block must be a
   <section>, give its padding a selector this reset cannot outrank. */
.feed-page section,
.account-page section,
#bt-callback section,
#bt-resolver section { padding: 0; }

/* --------------------------------------------------------------------------
   2. Skip link + focus
   -------------------------------------------------------------------------- */

/* The site had no skip link anywhere (accessibility defect 2). It is the
   first focusable thing on the page and stays invisible until focused. */
.skip-link {
  position: absolute;
  left: 0.75rem;
  top: 0.5rem;
  z-index: 50;
  transform: translateY(-200%);
  background: var(--gold);
  color: var(--navy-deep);
  font-family: var(--sans);
  font-size: 0.85rem;
  font-weight: 700;
  padding: 0.5rem 0.9rem;
  border-radius: 8px;
  text-decoration: none;
}
.skip-link:focus { transform: translateY(0); color: var(--navy-deep); }

/* Buttons here are real <button>s; style.css's :focus-visible ring applies to
   them, but they need the cursor and the site's font. */
button.btn,
.card-bell,
.card-expander,
.card-open,
.card-strip-glyph,
.vote-chip {
  font-family: var(--sans);
  cursor: pointer;
}
button[disabled],
.vote-chip[disabled] { cursor: default; }

/* THE FOCUS RING NEEDS 3:1 AGAINST WHAT IT SITS ON (WCAG 2.2 SC 1.4.11).
   style.css's ring is `3px solid var(--gold)` — 8.55:1 on the navy chrome it
   was designed for, but 1.51:1 on a white card and 1.46:1 on paper. These four
   pages are the first where nearly every focusable control (ref, pill, open,
   expander, breakdown, both chips, Show more) sits on white, so the ring is
   re-coloured in this scope only: navy is 12.0:1 on the card and 11.1:1 on
   paper, and the chrome above and below keeps its gold. */
.feed-page :focus-visible,
.account-page :focus-visible,
#bt-callback :focus-visible,
#bt-resolver :focus-visible { outline-color: var(--navy); }

/* Visually hidden, still announced. Used for the feed's announcement region —
   see the note on #bt-feed below. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   3. Feed page shell
   -------------------------------------------------------------------------- */

.feed-page {
  max-width: var(--feed-measure);
  margin: 0 auto;
  padding: 0 1rem 3.5rem;   /* 16px gutters on narrow screens */
  /* Reserve the first viewport before any card has rendered: the pre-hydration
     page is only the head + the loading state, which put the FOOTER inside the
     first paint — one visible frame of footer on every /us↔/eu hop (owner,
     2026-08-20). The honest states still sit at the top; only the footer is
     held below the fold. svh so a phone's collapsing URL bar doesn't overshoot. */
  min-height: 100vh;
  min-height: 100svh;
}

.feed-head { padding: 2.25rem 0 0; }

.feed-head .eyebrow { margin-bottom: 0.55rem; }

.feed-head h1 {
  font-family: var(--serif);
  font-size: clamp(1.95rem, 5.5vw, 2.5rem);
  font-weight: 650;
  line-height: 1.14;
  letter-spacing: -0.01em;
  color: var(--navy-ink);
  margin: 0 0 0.6rem;
}

.feed-intro {
  font-size: 1rem;
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 0 0 0.5rem;
  max-width: none;
}

.feed-explainer {
  font-size: 0.92rem;
  margin: 0 0 0.2rem;
  max-width: none;
}

/* The freshness line (mandatory — a page that cannot tell quiet from broken is
   the honesty hole the 2026-08-17 scope flagged). Empty until the JS fills it,
   so it must occupy no space when empty. */
.feed-fresh {
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--ink-faint-aa);
  margin: 0.9rem 0 0;
  max-width: none;
}
.feed-fresh:empty { display: none; }

.feed-fresh .fresh-warn { color: var(--gold-text); font-weight: 600; }

.feed {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 1.4rem;
}

/* The honest empty / health states. Dashed border is the site's own honesty-box
   grammar (style.css .log-empty), which COMPONENT-STYLE already names as the
   app's honesty box. Six states, the app's order and the app's words — loading,
   can't reach the feed, no matches, no posts yet, nothing on your docket,
   filters, nothing recent — rendered by the JS into this element. */
.feed-status {
  border: 1.5px dashed var(--line);
  border-radius: 10px;
  background: var(--card);
  padding: 1.2rem 1.35rem;
  margin-top: 1.4rem;
}
.feed-status:empty { display: none; }

.feed-status h2 {
  font-family: var(--serif);
  font-size: 1.12rem;
  font-weight: 650;
  color: var(--navy-ink);
  margin: 0 0 0.35rem;
}

.feed-status p {
  font-size: 0.93rem;
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 0;
  max-width: none;
}

/* The static "this page's script did not load" line that ships inside the
   loading state (#bt-boot-note). Quieter than the state's own sentence and
   below it, because it is a note ABOUT the page rather than the page's answer —
   the same weight `.acct-note` carries in the account page's loading region,
   which is the pattern it was copied from. The JS removes it on boot, so a
   reader only ever sees it when it is true.

   Written as a descendant so it OUTWEIGHS `.feed-status p` above (two classes
   beat one class + one type); as a bare class its margin and colour would both
   lose to that rule and the note would sit flush against the sentence. */
.feed-status .feed-status-note {
  font-size: 0.85rem;
  color: var(--ink-faint-aa);
  margin-top: 0.7rem;
}

.btn-more {
  display: block;
  margin: 1.35rem auto 0;
  background: var(--card);
  border-color: var(--line);
  color: var(--navy);
}
.btn-more:hover { background: var(--wash); }
.btn-more[hidden] { display: none; }

/* The no-JavaScript fallback. Plain, honest, and pointing at the pages that
   really are readable without scripts. */
.feed-noscript {
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  padding: 1.1rem 1.3rem;
  margin-top: 1.4rem;
  font-size: 0.93rem;
  line-height: 1.6;
  color: var(--ink-soft);
}
.feed-noscript p { margin: 0 0 0.5rem; max-width: none; }
.feed-noscript p:last-child { margin-bottom: 0; }

/* Build line — "skew is observable, never assumed" (plan §3). Sits under the
   feed, above the site footer. */
.feed-build {
  font-size: 0.72rem;
  color: var(--ink-faint-aa);
  margin: 2rem 0 0;
  text-align: center;
  max-width: none;
}
.feed-build:empty { display: none; }

/* --------------------------------------------------------------------------
   4. The card
   -------------------------------------------------------------------------- */

/* Glyphs. Stroke and stroke-width are inherited properties, so the referencing
   <svg> decides weight and colour and the sprite carries geometry only.

   ⚠️ WEIGHT COMES THROUGH `--glyph-stroke`, NOT A `stroke-width` ATTRIBUTE.
   The label table carries per-glyph weights that are owner law — ❌ is a bold
   `x` at 2.15 ("the most instantly-read no"), 🗳 ballot 1.95, 🤝 handshake
   1.55, ✅ circle-check 2 — and a CSS rule beats an SVG presentation
   attribute, so `svg.setAttribute('stroke-width', …)` is silently ignored
   here and every glyph would render at the class default. A custom property
   is the one channel that survives the cascade AND accepts a value the table
   invents tomorrow. The JS sets it on that one <svg>:
       svg.style.setProperty('--glyph-stroke', String(lp.stroke));
   Set it on the <svg>, never on a wrapper: it inherits, and the green corner
   badge lives in a sibling span. */
.glyph {
  width: 20px;
  height: 20px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: var(--glyph-stroke, 2);
  stroke-linecap: round;
  stroke-linejoin: round;
}
.glyph--filled { fill: currentColor; stroke: none; }

.card {
  position: relative;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--card-radius);
  padding: 13px 15px;
  box-shadow: 0 1px 4px rgba(10, 49, 97, 0.05);
}

/* Per-polity accents — the content law: a post keeps its OWN polity colours
   forever, whatever page it is shown on (COMPONENT-STYLE, "the theme is the
   building, posts are the residents"). --glyph-band paints the calendar
   glyph's header band through the <use> shadow tree: US red, EU gold, the
   twin-card hairline. */
.card[data-polity="us"] {
  --accent: var(--navy);
  --accent-wash: var(--wash);
  --accent-chip-text: var(--navy-ink);
  --glyph-band: var(--red);
}
.card[data-polity="eu"] {
  --accent: var(--royal);
  --accent-wash: var(--wash-eu);
  --accent-chip-text: var(--royal-deep);
  --glyph-band: var(--gold);
}

/* deep-linked / highlighted card */
.card.is-highlighted { border-color: var(--accent); border-width: 1.5px; padding: 12.5px 14.5px; }

/* --- head row ------------------------------------------------------------ */

.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
}

.card-ref-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  flex-wrap: wrap;
}

.card-ref {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 13.5px;
  line-height: 1.3;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.card-ref:hover { color: var(--accent); }

/* --- the follow bell ------------------------------------------------------
   ONE TAP TO PUT A BILL ON YOUR DOCKET, and on this surface what that buys is
   the header's notices panel - there is no web push (spec, 2026-08-20). It sits
   immediately after the reference because that is the bill it is about: the
   accessible name says so too, and a bell floating at the other end of the row
   would be a control with no visible subject.

   THE SHAPE IS THE STATE, not the colour (owner, 2026-07-22): bell-plus when
   you do not follow it, the filled bell when you do. The accent underneath is
   an accompaniment, so nothing here depends on colour vision - the same two
   glyphs the phone draws, out of the same sprite.

   Geometry is `.card-open`'s law again ("padding P, margin -(P - 2)"): the
   glyph draws at 18px, 9px of padding makes a 36px hit box, and -7px of margin
   hands the layout back its 22px so the reference row keeps the rhythm it had.
   The overhang stays inside the row's own 10px gaps, so pressing near the
   reference link never lands on the bell. */
.card-bell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  padding: 9px;
  margin: -7px;
  background: none;
  border: 0;
  border-radius: 8px;
  line-height: 0;
  color: var(--ink-faint-aa);
}
.card-bell .glyph { width: 18px; height: 18px; stroke-width: 1.7; }
.card-bell[aria-pressed="true"] { color: var(--accent); }
.card-bell:hover:not([disabled]) { color: var(--accent); }
/* DISABLED IS A DIFFERENT STATEMENT HERE THAN ON A CHIP, so it is styled
   differently on purpose. A disabled chip has to be READ - its words carry the
   claim "we cannot key this post, so voting is off" - which is why feed.css
   keeps it at AA rather than fading it. This control carries no claim at all:
   an unkeyable post is drawn with NO bell (render/card.ts), so the only thing a
   disabled bell ever says is "the page's JavaScript has not arrived", and the
   one thing it must not look is pressable. Fading to the card's own hairline
   ink is the honest shape - visibly present, visibly off - and WCAG exempts a
   disabled control from contrast for exactly this case. */
.card-bell[disabled] { color: var(--line); }

/* Franchise / batch cards lead with the date instead of a reference. */
.card-date-lead {
  display: flex;
  align-items: center;
  gap: 4px;
  font-family: var(--sans);
  font-size: 12.5px;
  color: var(--ink-faint-aa);
  min-width: 0;
}
.card-date-lead .glyph { width: 17px; height: 17px; stroke-width: 1.7; }

/* THE HEAD-RIGHT SLOT IS AN "OPEN", NOT A "MENU" — the one place this card
   deliberately differs from the app's, and it differs because the surfaces
   differ. In the app that slot is ⋯: it opens a sheet (share, report an error,
   open the source). The web has no such sheet, and the plan gives the web its
   own navigation instead (D27(f): "on the web a card opens /p/<id>"). Drawing
   ⋯ for a control that navigates would be the dishonest-affordance pattern
   this product refuses — an 18 px unlabelled menu glyph as the *only*
   feed→post edge. Same slot, same size, same quiet ink; a disclosure chevron,
   which is what it does. When the web grows a real overflow menu, ⋯ comes
   back here and this moves. */
/* ⚠️ THE PADDING AND THE NEGATIVE MARGIN ARE ONE NUMBER, AND THEY CANCEL.
   The chevron DRAWS at 18 px and used to be TAPPED at 22 px (18 + 2 px padding
   each side) — under WCAG 2.5.5's 44 px and, on the feed, the only edge from a
   card to its post page (estate review R2, 2026-08-20). 13 px of padding takes
   the hit box to 44 × 44; -11 px of margin takes the LAYOUT box back to the
   22 × 22 it always occupied, so the glyph lands on exactly the pixel it landed
   on before — the head row is `align-items: center` / `space-between`, and both
   place the outer margin box, which is unchanged. Nothing above or below moves.

   The 11 px it overhangs stays inside the card's own 13/15 px padding, so it
   never reaches the card border and cannot widen the page. On its left it
   claims the head row's 8 px gap; only a ref row that runs the full width puts
   anything within the remaining 3 px, and what sits there is a pill's trailing
   padding, never its text. Keep the two numbers in step: padding P, margin
   -(P - 2).

   One thing this does change on purpose: the focus ring now traces the 44 px
   box rather than the glyph, because that is where the control actually is. A
   ring that draws smaller than the target is a ring that describes it wrongly. */
.card-open {
  background: none;
  border: 0;
  padding: 13px;
  margin: -11px;
  line-height: 0;
  color: var(--ink-faint-aa);
  flex: none;
}
.card-open .glyph { width: 18px; height: 18px; }

/* --- header pills -------------------------------------------------------- */

/* One pill per header (correction flags win the slot; the older-action FOOTER
   still renders below whether or not its pill did — COMPONENT-STYLE). */
.pill-correction,
.flag-older {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 10px;
  line-height: 1.6;
  letter-spacing: 0.9px;
  text-transform: uppercase;
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 1px 7px;
  white-space: nowrap;
  flex: none;
  text-decoration: none;
}
/* "Inaccurate" names a problem, "Correction" names the fix — never a pair that
   shares a stem (COMPONENT-STYLE, litigated). flagRed marks OUR mistake and is
   not the negative-outcome rule breaking: a bill's fate is never coloured. */
.pill-correction[data-kind="inaccurate"] { color: #A32D2D; }
.pill-correction[data-kind="correction"] { color: #1A7F37; }
.flag-older { color: var(--gold-text); }

/* --- collapsing body (D6) ------------------------------------------------ */

/* Only the BODY collapses. The head row above and the vote strip below stay
   anchored — clipping either would make expanding the card the only way to
   reach the ⋯ menu or vote. */
.card-body { position: relative; }

.card-body[data-clipped="1"] { max-height: 168px; overflow: hidden; }

/* Softened clip edge — no hard cut through a line of text. */
.card-body[data-clipped="1"]::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 34px;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--card));
}

.card.is-open .card-body { max-height: none; overflow: visible; }
.card.is-open .card-body::after { content: none; }

/* Always behind the expander, never clipped (structure, not measurement: a
   summary box is ~60-70px and never once fitted). */
.card-deferred { display: none; }
.card.is-open .card-deferred { display: block; }

/* --- status card body ---------------------------------------------------- */

/* font-weight is EXPLICIT: the card is a heading element here (§ card DOM), and
   style.css's h2 rule would otherwise impose the site's 650 display weight. The
   app's card serif is `SourceSerif4_600SemiBold` — 600. */
.card-title {
  font-family: var(--serif);
  font-size: 17px;
  font-weight: 600;
  line-height: 23px;
  color: var(--navy-ink);
  margin: 0 0 3px;
  max-width: none;
}

/* THE TITLE CLAMPS; THE STATUS LINE NEVER DOES. Bill titles are legal
   boilerplate and run to 380 characters — on this product the status is the
   news and the title is context, so the title is what gives way.

   THE CONDITION IS "COLLAPSED", NOT "CLIPPED", and the app is the reason for
   both halves: `numberOfLines={expanded ? undefined : 3}` (post-card.tsx) —
   it clamps whenever the card is closed, whether or not the body overflows,
   and it shows the FULL TITLE ON EXPAND. Keying the clamp to data-clipped got
   the first half wrong; leaving it un-lifted got the second half wrong, and
   the second is the serious one: the bottom half of a 382-character title was
   unreachable on the web. */
.card:not(.is-open) .card-title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

.card-meta {
  font-family: var(--sans);
  font-size: 12px;
  line-height: 17px;
  color: var(--ink-soft);
  margin: 0 0 10px;
  max-width: none;
}

.card-action {
  font-size: 14.5px;
  line-height: 22.5px;
  color: var(--ink);
  margin: 0 0 6px;
  max-width: none;
}

/* Glyphs ride INSIDE the text flow so wrapping uses the full card width.
   :where() again — this rule must not out-specify the per-context ones below
   that adjust the same properties. */
:where(.card-action, .card-headline) .glyph {
  display: inline-block;
  vertical-align: -4px;
}
/* Context rules sit inside :where() so they carry the specificity of the class
   alone (0,1,0). Written as plain descendants, `.card-headline .glyph-label`
   (0,2,0) tied with `.glyph-label.is-green` and won on source order — the
   green ✅ family rendered navy in a headline, and "colour only for the good
   news" is a named law, not a nicety. */
:where(.card-action) .glyph-cal {
  width: 18.5px;
  height: 18.5px;
  --glyph-stroke: 1.7;
  color: var(--ink-soft);
}
:where(.card-action) .glyph-label {
  width: 20px;
  height: 20px;
  --glyph-stroke: 1.7;
  color: var(--accent);
}
/* Colour only for the good news: passage/adoption may carry meaning through
   colour; a rejection, veto or withdrawal stays neutral ink. Never red. */
.glyph-label.is-green { color: #1A7F37; }

.card-action-date { font-family: var(--sans); color: var(--ink-soft); }
.card-action-label { font-family: var(--sans); font-weight: 600; color: var(--ink); }
.card-action-tally { font-family: var(--sans); color: var(--ink-soft); }

/* Badge-bearing glyphs (✅🏛️ passed a chamber, 📜 became law) carry a small
   green check at the corner; the badge overhangs, so the wrapper reserves
   room before the text that follows. */
.glyph-wrap { position: relative; display: inline-block; vertical-align: -4px; }
.glyph-wrap.has-badge { padding-right: 5px; }
.glyph-wrap .glyph { vertical-align: top; }
.glyph-badge {
  position: absolute;
  top: -3.6px;
  right: 0;
  width: 12px;
  height: 12px;
  border-radius: 6px;
  background: #1A7F37;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* The badge's tick keeps a literal weight: it is fixed by the badge's own
   geometry and must never inherit a label glyph's table weight. */
.glyph-badge .glyph {
  width: 8px;
  height: 8px;
  stroke-width: 3.8;
  color: #fff;
}

.card-source {
  font-family: var(--sans);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ink-faint-aa);
  margin: 8px 0 0;
  max-width: none;
}

/* --- franchise card body ------------------------------------------------- */

.card-headline {
  font-family: var(--serif);
  font-size: 16.5px;
  font-weight: 600;   /* the app's card serif — see .card-title */
  line-height: 26px;
  color: var(--navy-ink);
  margin: 0 0 8px;
  max-width: none;
}
:where(.card-headline) .glyph-label {
  width: 24px;
  height: 24px;
  --glyph-stroke: 1.7;
  color: var(--accent);
  vertical-align: -5px;
}
:where(.card-headline) .glyph-wrap { vertical-align: -5px; }

.card-body-lines { display: flex; flex-direction: column; gap: 6px; }

.card-body-lines p {
  font-family: var(--sans);
  font-size: 14px;
  line-height: 20.5px;
  color: var(--ink);
  margin: 0;
  max-width: none;
}

/* --- anchored: what's next + budget -------------------------------------- */

/* ANCHORED, NOT CLIPPED. "What's next" is the answer to the question a reader
   of a legislative feed actually has, and it was being decided by title
   length — short-titled bills showed it, three-line resolution titles ate it. */
.card-next {
  font-family: var(--sans);
  font-size: 14px;
  line-height: 20.5px;
  color: var(--ink);
  margin: 0 0 2px;
  max-width: none;
}
.card-budget {
  font-family: var(--sans);
  font-size: 13px;
  line-height: 19px;
  color: var(--ink-soft);
  margin: 4px 0 0;
  max-width: none;
}
.lead { font-weight: 600; }

/* --- deferred: summary, older note, quote -------------------------------- */

.card-summary {
  background: var(--accent-wash);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
  padding: 9px 11px;
  margin-top: 9px;
}
.card-summary p {
  font-family: var(--sans);
  font-size: 13.5px;
  line-height: 20px;
  color: var(--ink);
  margin: 0;
  max-width: none;
}
/* ALWAYS, with every summary. Not a conditional. */
.card-summary .ai-tag {
  display: block;
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.4;
  color: var(--ink-faint-aa);
  margin-top: 4px;
}

/* Footnote register: inkSoft rather than faint, because this one is meant to
   be READ. Cause-neutral wording is the bot's own house rule. */
.card-older-note {
  font-family: var(--sans);
  font-size: 11px;
  line-height: 16px;
  color: var(--ink-soft);
  margin: 8px 0 0;
  max-width: none;
}

/* The quote shape: an inset, left-ruled reference to the corrected post.
   Single-sided border ⇒ NO radius (COMPONENT-STYLE) — rounded corners only
   work with a full border. */
.card-quote {
  display: block;
  border-left: 3px solid var(--accent);
  border-radius: 0;
  background: var(--paper);
  padding: 6px 0 6px 10px;
  margin-top: 10px;
  text-decoration: none;
  color: inherit;
}
.card-quote-tag {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 10px;
  line-height: 1.4;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--ink-faint-aa);
  margin: 0 0 3px;
  max-width: none;
}
.card-quote-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.card-quote-ref { font-family: var(--sans); font-weight: 700; font-size: 13px; color: var(--accent); }
.card-quote-date { font-family: var(--sans); font-size: 12px; color: var(--ink-faint-aa); }
.card-quote-label {
  font-family: var(--sans);
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--ink-soft);
  margin: 2px 0 0;
  max-width: none;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.card-quote-tally { color: var(--ink-faint-aa); }
.card-quote-missing { font-family: var(--sans); font-size: 12.5px; color: var(--ink-faint-aa); margin: 0; }

/* --- expander ------------------------------------------------------------ */

.card-expander {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: 0;
  padding: 9px 0 1px;
  font-weight: 600;
  font-size: 13px;
  color: var(--accent);
}
.card-expander .glyph { width: 15px; height: 15px; stroke-width: 2.2; }
.card-expander[hidden] { display: none; }

/* --- action strip -------------------------------------------------------- */

/* Opposite edges on purpose: the breakdown glyph at the far left — the "see
   where it stands" anchor — and the chips at the far right, the action zone.
   On a post with no bill identity the glyph is ABSENT (there is no tally to
   open — README rule 2), leaving .vote-chips as the strip's only child. It
   still lands in the same place: .vote-reason's `flex-basis: 100%` makes the
   chip row span the strip, and its own `justify-content: flex-end` holds the
   two pills at the right edge — measured identical to a keyed card's, to the
   pixel. So a refused card reads as the same card with its chips off, not as a
   differently-built one. */
.card-strip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--line-soft);
}

.card-strip-glyph {
  background: none;
  border: 0;
  padding: 2px;
  margin-right: 9px;
  line-height: 0;
  color: var(--ink-faint-aa);
  flex: none;
}
.card-strip-glyph .glyph { width: 16px; height: 16px; stroke-width: 1.7; }

/* Vote chips — accuracy guard, owner-locked. "▲ Yea · ▼ Nay" (US) /
   "▲ For · ▼ Against" (EU). Never hearts, never thumbs, never a net score,
   and no counts on the chip on ANY surface. Text arrows, not icons. */
.vote-chips {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.vote-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 4px 11px;
  background: var(--card);
  font-family: var(--sans);
  font-weight: 600;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-soft);
}
.vote-chip[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-wash);
  color: var(--accent-chip-text);
}
/* DELIBERATE DIVERGENCE FROM THE APP, recorded so it is a decision and not
   drift: `vote-chips.tsx` disables a chip with `opacity: 0.45`, which on this
   palette lands around 2.4:1 — unreadable, and this is the one chip state that
   has to be READ, because it is the state whose whole job is to say "we can't
   key this post, so voting is off". The web keeps the same two signals (muted
   text, softened border) at AA instead. If the app ever adopts a readable
   disabled state, they converge here. */
.vote-chip[disabled] { color: var(--ink-faint-aa); border-color: var(--line-soft); }

/* The caption slot — ON THE CHIPS' ROW, left of them, after the breakdown glyph.

   ⚠️ THIS MOVED ON 2026-08-19 AND THE OLD REASONING IS RECORDED BELOW, because
   it was right about its own case and wrong about the one that matters. It used
   to be a child of .vote-chips with `flex-basis: 100%`, taking a line of its own
   UNDER the chips — argued for on the grounds that NO_IDENTITY_REASON is a full
   sentence and squeezing it beside two pills makes it look like an error toast.
   True of that sentence. But the caption a reader actually meets is four words,
   "Sign in to vote", and it is the strip's primary invitation: owner directive,
   2026-08-19 — "it should be 'Sign in to vote' and it should be on the same row
   as the buttons". It is also where the APP has always put it (vote-chips.tsx
   renders the caption as the third child of the chip ROW, shrinkable), so the
   web was the outlier.

   So the slot is now a child of .card-strip and the row reads
   [glyph] [caption] … [chips]. `flex: 1` claims the gap between them, and
   `min-width: 0` is what lets the long refusal WRAP inside that gap instead of
   shoving the chips off the card — the same job `flexShrink: 1` does on the
   phone. A two-line refusal simply makes the strip two lines tall, which is
   what it does on the phone too. */
.vote-reason {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.45;
  color: var(--ink-faint-aa);
  margin: 0;
  max-width: none;
  text-align: left;
}

/* Nothing to say is a state, not a gap: an empty slot must claim no width at
   all, or a signed-in reader's strip carries a hole where a caption was. */
.vote-reason:empty { display: none; }

/* Narrow phones: the strip stacks rather than squeezing the chips, and the
   caption takes the first line on its own. */
@media (max-width: 24rem) {
  .card-strip { flex-wrap: wrap; }
  .vote-reason { flex-basis: 100%; }
  .vote-chips { justify-content: flex-start; }
}

/* --------------------------------------------------------------------------
   5. Account / callback / resolver
   -------------------------------------------------------------------------- */

.account-page,
#bt-callback,
#bt-resolver {
  max-width: 34rem;
  margin: 0 auto;
  padding: 2.5rem 1rem 4rem;
}

.account-page h1,
#bt-callback h1,
#bt-resolver h1 {
  font-family: var(--serif);
  font-size: clamp(1.7rem, 4.5vw, 2.1rem);
  font-weight: 650;
  line-height: 1.18;
  color: var(--navy-ink);
  margin: 0 0 0.7rem;
}

.acct-region { margin-top: 1.6rem; }
.acct-region[hidden] { display: none; }

.acct-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 1.2rem 1.35rem;
}

.acct-card + .acct-card { margin-top: 1rem; }

.acct-card h2 {
  font-family: var(--serif);
  font-size: 1.15rem;
  font-weight: 650;
  color: var(--navy-ink);
  margin: 0 0 0.45rem;
}

.acct-card p { font-size: 0.94rem; line-height: 1.6; color: var(--ink-soft); margin: 0 0 0.6rem; max-width: none; }
.acct-card p:last-child { margin-bottom: 0; }

.acct-label {
  font-family: var(--sans);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint-aa);
  margin: 0 0 0.2rem;
}

.acct-value { font-size: 0.98rem; color: var(--ink); margin: 0; word-break: break-word; }

.acct-buttons { display: flex; flex-direction: column; gap: 0.65rem; margin-top: 0.9rem; }

.btn-provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  width: 100%;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.7rem 1rem;
  font-family: var(--sans);
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--ink);
}
.btn-provider:hover:not([disabled]) { background: var(--wash); }
/* One width for both doors (owner catch on the live page, 2026-08-23: Google
   caps its rendered button at 400px, so it sat visibly narrower than the
   full-width buttons around it). The whole sign-in column meets Google at its
   ceiling: the mount is capped so the measured width handed to renderButton
   IS 400, and the plain buttons wear the same cap — parity by agreement, not
   by hoping. Scoped to the signed-out card; the signed-in tail and the delete
   card keep their own widths. */
#bt-acct-signed-out .acct-buttons .btn-provider,
#bt-gsi-google {
  width: 100%;
  max-width: 400px;
  margin-inline: auto;
}
#bt-gsi-google { display: flex; justify-content: center; }
/* The Google doors' single-slot stack (owner flicker catch #2, 2026-08-23:
   the swap used to be two layout jumps — Google's button popping in below,
   then the plain one collapsing away). Both doors share one grid cell;
   Google's renders IN PLACE but invisible (visibility keeps the box, so the
   width measurement and the appeared-check still work), and .is-gsi reveals
   it in the same frame the plain one leaves — one repaint, zero shift. The
   min-height pins the cell to the plain door's height so even the few-pixel
   difference between the two buttons cannot move the page. */
/* FIXED height + clip, not a minimum (owner catch #4, 2026-08-24: Google's
   iframe transiently grew during its own re-render and pushed the Apple
   button down for a beat — a min-height is a floor, and the wobble punched
   through it). Every GIS button variant is 40px tall; a sealed 44px cell
   means whatever happens inside Google's frame happens inside this box, and
   the page below it physically cannot move. */
.google-door { display: grid; align-items: center; height: 44px; overflow: hidden; }
.google-door > * { grid-area: 1 / 1; }
#bt-gsi-google { visibility: hidden; }
.google-door.is-gsi #bt-gsi-google { visibility: visible; }
.btn-provider[disabled] { color: var(--ink-faint-aa); background: var(--paper); }
/* ⚠️ AN AUTHOR `display` BEATS THE UA'S `[hidden] { display: none }` WHATEVER
   THE SPECIFICITY — author origin wins over user-agent origin — so the
   `display: flex` above would keep a `hidden` provider button on screen. That
   is the trap style.css already spells out for the notices bell in the site nav
   — named by description, not by selector, because check-site-nav.js asserts
   that bell's rules live in style.css by grepping THIS file for them, and a
   comment that can break a guard is worse than one that can satisfy it. It is
   also the companion rule every other hide-able component here already carries
   (`.acct-region[hidden]`, `.geo-field[hidden]`, `.bt-backdrop[hidden]`).
   It became load-bearing on 2026-08-23: when Google's OWN button renders,
   src/web/account.ts hides `#bt-signin-google` — without this line the reader
   would see TWO "Continue with Google" doors, one of them the redirect flow
   whose consent screen names supabase.co, which is the exact thing that
   change-set exists to remove. */
.btn-provider[hidden] { display: none; }

/* When no provider is configured the buttons are disabled and this line says
   so. Inviting a sign-in that does not exist is the dishonest-affordance
   version of the very thing this product guards against. */
.acct-note { font-size: 0.85rem; color: var(--ink-faint-aa); margin: 0.7rem 0 0; max-width: none; }
/* #bt-provider-note ships empty and is filled only when a sign-in could not be
   started (src/web/account.ts). Empty must take no room at all, or the page
   carries a gap that looks like a missing sentence. */
.acct-note:empty { display: none; }

.acct-danger { margin-top: 1.6rem; font-size: 0.9rem; }
.acct-danger a { color: var(--navy); }

.acct-status { font-size: 0.93rem; color: var(--ink-soft); margin: 0.9rem 0 0; }
.acct-status:empty { display: none; }

/* The Settings section — the app's Settings screen on /account (2026-08-22;
   src/web/settings.ts renders the groups into #bt-settings-groups, reusing the
   Display panel's own .bt-row/.bt-sw/.bt-chips tree, which this stylesheet
   already styles above). Only what that reuse does not cover is added here. */

/* The switches' checked colour. `--accent` is otherwise defined only by a
   card's or a feed page's [data-polity] — the two display toggles here are
   cross-legislature device prefs, so they wear the site's own chrome navy,
   never one legislature's colour. Without this the checked track would paint
   TRANSPARENT (an undefined custom property invalidates the declaration). */
.account-page { --accent: var(--navy); }

/* The section-padding reset above zeroes any padding-top given here (it
   outranks one-class rules — see the fence at the top of this file), so the
   gap under the rule line rides on the heading's own margin instead; the
   section's border blocks margin collapse, so it holds. */
.acct-settings { margin-top: 2.4rem; border-top: 1px solid var(--line); }
.acct-settings > h2 {
  font-family: var(--serif);
  font-size: 1.35rem;
  font-weight: 650;
  color: var(--navy-ink);
  margin: 1.5rem 0 0;
}

/* The group eyebrows — the app Settings screen's own grammar (uppercase small
   caps above a card), .acct-label's look freed from its in-card margins. */
.acct-eyebrow {
  font-family: var(--sans);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint-aa);
  margin: 1.3rem 0 0.45rem;
}

/* The Privacy card's quiet doors. */
.acct-links { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.acct-links a { font-size: 0.94rem; color: var(--navy); }

/* (The temporary signed-in preview's `.acct-linklike` rule stood here until
   2026-08-22. It was deleted with the module that drew the link, in the
   change-set that enabled a real sign-in provider — the trigger its own header
   carried. `check:webauth` proves the sweep, this file included: while the
   module existed the rule had to exist, and now that it is gone the rule must
   stay gone. No other class ever used it.) */

#bt-callback .auth-status,
#bt-resolver .resolver-status {
  font-size: 1rem;
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 0 0 1rem;
  max-width: none;
}

.page-links { list-style: none; padding: 0; margin: 1.2rem 0 0; }
.page-links li { margin: 0.45rem 0; }

/* The 404 resolver's own "As published" block. The archived text inside it is
   `.post` / `.post-text`, which style.css already sets `white-space: pre-line`
   on, so only the disclosure around it needs saying. */
.resolver-archive {
  margin-top: 1rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--paper);
}
.resolver-archive > summary {
  cursor: pointer;
  padding: 0.6rem 0.9rem;
  font-family: var(--sans);
  font-size: 0.85rem;
  color: var(--ink-soft);
}
.resolver-archive .post { padding: 0 0.9rem 0.9rem; }

/* The legacy `/bill/<ref>` pick, when a reference names more than one bill (a
   rollover pair): the title under each row is what tells two "H.R. 3497" rows
   apart, beside the Congress the link itself names. */
.resolver-pick-title {
  display: block;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   6. What the vote island adds (README section 2b)
   -------------------------------------------------------------------------- */

/* WRITTEN 2026-08-19 BY THE INTEGRATION LANE, against the DOM the island
   actually emits (`src/web/{votes,consent,breakdown,geo-picker,account}.ts`).
   README 2b specified these classes before the styling existed, and two of them
   were reachable by any visitor the moment the bundle shipped, with nothing to
   draw them:

     - `.vote-caption.is-pulse` is the whole answer to a signed-out chip tap.
       The island adds the class for 800 ms and does nothing else, deliberately
       (the caption's words never change mid-tap, owner 2026-07-23). With no
       rule the tap was SILENT, which is the one thing that state must not be.
     - `.vote-breakdown` opens for every reader, signed in or not, and drew an
       unstyled block of text under the card.

   The look is the app's, not a new one: `vote-breakdown.tsx`'s track is
   --line-soft with the polity accent as the fill, its "you" tag is gold, its
   stats are bordered tiles. */

/* --- the caption ---------------------------------------------------------- */

/* It inherits size and colour from `.vote-reason`, the slot it lives in (one
   slot, fixed precedence). Only the link-ness and the pulse are its own.

   --gold-wash is the pulse's colour, derived here from the palette's --gold
   the same way the underline below derives from --accent — the gold itself
   stays written once, in style.css. */
.vote-caption {
  color: var(--accent);
  font-weight: 600;
  border-radius: 5px;
  padding: 1px 4px;
  margin: -1px -4px;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  --gold-wash: color-mix(in srgb, var(--gold) 45%, transparent);
}
.vote-caption:hover { border-bottom-color: var(--accent); }

/* ONE sweep, 800 ms - the same 800 ms the island holds the class for, so the
   emphasis ends with it rather than being cut off mid-animation.
   GOLD, not the accent wash (owner, 2026-08-20): gold is the site's one visual
   word for sign-in and the account (the nav pill, the notices panel's pill),
   and this flash answers a press whose remedy IS sign-in — the pale accent
   wash read as "something happened" where it should say "the door is here". */
@keyframes bt-caption-pulse {
  0%   { background: transparent; }
  25%  { background: var(--gold-wash); }
  100% { background: transparent; }
}
.vote-caption.is-pulse { animation: bt-caption-pulse 800ms ease-out; }

/* A reader who asked for less motion still has to SEE that the tap was
   answered, so the emphasis stays and only the animation goes. The class is
   removed after 800 ms either way, which is what makes a static wash read as a
   flash rather than as a new permanent state. */
@media (prefers-reduced-motion: reduce) {
  .vote-caption.is-pulse { animation: none; background: var(--gold-wash); }
}

/* --- the Article 9 consent step ------------------------------------------ */

/* It appears INSIDE the strip that raised it, and it is a question - so it is
   bordered like one and not styled like a banner. */
.vote-consent {
  flex-basis: 100%;
  margin-top: 8px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--paper);
  text-align: left;
}
.vote-consent-text {
  font-family: var(--sans);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0 0 9px;
  max-width: none;
}
.vote-consent-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.vote-consent-btn {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 13px;
  background: var(--card);
  font-family: var(--sans);
  font-weight: 600;
  font-size: 12.5px;
  color: var(--ink-soft);
}
/* "Agree and vote" is the affirmative, so it carries the accent - but it is a
   consent button, so it is not the loudest thing on the card. */
.vote-consent-btn.is-agree {
  border-color: var(--accent);
  background: var(--accent-wash);
  color: var(--accent-chip-text);
}

/* --- the breakdown panel -------------------------------------------------- */

/* A panel, not a dialog (README 2b): it opens in place after the strip, inside
   the card, and takes the card's full width back from the strip's flex row. */
.vote-breakdown {
  margin-top: 10px;
  padding: 11px 12px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--paper);
}
.vb-eyebrow {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 9.5px;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--ink-faint-aa);
  margin: 0 0 2px;
  max-width: none;
}
.vb-title {
  font-family: var(--serif);
  font-size: 17px;
  line-height: 1.25;
  color: var(--navy-ink);
  margin: 0 0 11px;
  max-width: none;
}

/* The three states are kept visually apart on purpose: a failure must never be
   mistaken for a result (`vote-breakdown-model.ts`). */
.vb-message { padding: 6px 0 2px; text-align: center; }
.vb-message-title {
  font-family: var(--serif);
  font-size: 15px;
  color: var(--navy-ink);
  margin: 0 0 4px;
  max-width: none;
}
.vb-message-body {
  font-family: var(--sans);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-faint-aa);
  margin: 0 auto;
  max-width: 30rem;
}
.vb-retry {
  margin-top: 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 4px 13px;
  background: var(--card);
  font-family: var(--sans);
  font-weight: 600;
  font-size: 12.5px;
  color: var(--accent);
}

.vb-stats { display: flex; gap: 9px; margin-bottom: 11px; }
.vb-stat {
  flex: 1;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--card);
  padding: 8px 10px;
}
/* The reader's own place is pinned and never collapsed into the tail, so it is
   the one tile that wears the accent. */
.vb-stat.is-inpolity { border-color: var(--accent); background: var(--accent-wash); }
.vb-stat-label {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 9.5px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-faint-aa);
  margin: 0 0 1px;
  max-width: none;
}
.vb-stat-count {
  font-family: var(--serif);
  font-size: 21px;
  line-height: 1.15;
  color: var(--navy-ink);
  margin: 0;
  max-width: none;
}
.vb-stat-split {
  font-family: var(--sans);
  font-size: 11.5px;
  color: var(--ink-soft);
  margin: 1px 0 0;
  max-width: none;
}

.vb-coverage {
  font-family: var(--sans);
  font-size: 11.5px;
  color: var(--ink-faint-aa);
  margin: 0 0 9px;
  max-width: none;
}

.vb-rows { list-style: none; padding: 0; margin: 0; }
.vb-row { padding: 6px 2px; }
/* A group boundary is a hairline, not a heading: the rows are one list and the
   break only says "a different kind of place starts here". */
.vb-row[data-group-start] {
  margin-top: 4px;
  padding-top: 9px;
  border-top: 1px solid var(--line-soft);
}
.vb-row-head { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
/* .vb-flag retired 2026-08-23 with the emoji flags (owner ruling; geo.ts
   header in the app repo) — every place-marker is the .vb-badge now. */
.vb-badge {
  min-width: 26px;
  padding: 0 4px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--wash);
  font-family: var(--sans);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.4px;
  line-height: 17px;
  text-align: center;
  color: var(--ink-soft);
}
/* A residual row ("Rest of world") stands for no place in particular, so its
   marker is drawn as an absence rather than as another badge. */
.vb-badge.is-residual { background: transparent; border-style: dashed; }
.vb-label {
  flex: 1;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 13.5px;
  color: var(--ink);
}
.vb-you {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 9.5px;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--gold-text);
}
/* BOTH SIDES, ALWAYS - never a net score (D14, README 2b). */
.vb-split { font-family: var(--sans); font-weight: 600; font-size: 12.5px; color: var(--ink); }
.vb-split-down { font-weight: 400; color: var(--ink-faint-aa); }
/* n IS NOT DECORATION. It is what keeps a one-person row honest; do not hide it
   to tidy the row (D14). */
.vb-total {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 13px;
  color: var(--ink);
  min-width: 32px;
  text-align: right;
}
.vb-track {
  height: 7px;
  border-radius: 4px;
  background: var(--line-soft);
  overflow: hidden;
}
/* The width is DATA, so it arrives as a custom property on the element - the
   same sanctioned exception as --glyph-stroke (README rule 11). */
.vb-fill { width: var(--vb-fill, 0%); height: 100%; background: var(--accent); }

.vb-more { padding: 6px 0 2px; text-align: center; }
.vb-more-btn {
  background: none;
  border: 0;
  padding: 4px 6px;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 12.5px;
  color: var(--accent);
}

.vb-legend {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 11px 0 0;
  font-family: var(--sans);
  font-size: 11.5px;
  color: var(--ink-soft);
  max-width: none;
}
.vb-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
.vb-dot.is-up { background: var(--accent); }
.vb-dot.is-down { background: var(--line-soft); border: 1px solid var(--line); }
/* The legend is one line of "dot word dot word"; the second pair needs the gap
   the first one gets from the flex `gap` alone. */
.vb-legend .vb-dot ~ .vb-dot { margin-left: 8px; }

.vb-foot {
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-faint-aa);
  margin: 8px 0 0;
  max-width: none;
}

/* --- the account page's location picker and delete card ------------------- */

.geo-picker { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-top: 0.6rem; }
.geo-field { display: flex; flex-direction: column; gap: 0.25rem; min-width: 11rem; flex: 1; }
.geo-field[hidden] { display: none; }
.geo-label {
  font-family: var(--sans);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint-aa);
}
.geo-select {
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.5rem 0.6rem;
  background: var(--card);
  font-family: var(--sans);
  font-size: 0.95rem;
  color: var(--ink);
}
.geo-select[disabled] { color: var(--ink-faint-aa); background: var(--paper); }
.geo-status { flex-basis: 100%; }
.geo-status:empty { display: none; }

/* The picker's type-ahead and the location disclosure under it (app repo
   `src/web/geo-picker.ts`, 2026-08-23). The search field is deliberately a
   `.geo-select` — same box, same type as the control it filters — and needs no
   rule of its own; `.geo-block` is the wrapper that lets the note sit UNDER the
   fields' flex line instead of inside it. What is left is the disclosure
   toggle: every other <details> on this site names its own cursor
   (`.resolver-archive > summary`, style.css `details.blurbs summary`), and the
   open panel needs air under the word it came from. The summary's TYPE is
   `.acct-note`'s, set in the markup — quieter than the sentence above it, which
   is the point of a "more". */
.geo-more summary { cursor: pointer; }
.geo-more[open] summary { margin-bottom: 0.35rem; }

/* Owner sizing pass, 2026-08-23 evening: "a bit more space between the
   dropdown and the start of the text, and the font can be a touch smaller —
   that also applies to the saved text." The whole note stack under the
   controls — disclosure, its More layer, and the save status — steps down
   together so the block reads as one quiet footer rather than three sizes. */
.geo-disclosure { margin-top: 1.1rem; }
.geo-disclosure,
.geo-status,
.geo-more summary,
.geo-more p { font-size: 0.8rem; }

/* Deleting an account is irreversible, so its card is marked - and its confirm
   button is the only red thing on the page. */
.acct-delete { border-color: color-mix(in srgb, var(--red) 35%, var(--line)); }
.btn-provider.is-danger { border-color: var(--red); color: var(--red); }
.btn-provider.is-danger:hover:not([disabled]) {
  background: color-mix(in srgb, var(--red) 7%, var(--card));
}

/* --------------------------------------------------------------------------
   6. The feed bar — search, the legislature selector, the display controls
      (SEARCH-JOB-SPEC-2026-08-20 §6-§8; drawn from the owner-reviewed mock)

   ONE bar, and it STICKS. A searched or filtered feed that looks like the
   plain feed is a lie by omission, so the query and the filtered-state strip
   stay on screen while the reader scrolls. The site header deliberately does
   not stick with it: one band of fixed chrome, never two.

   No breakpoint decides the layout — `flex-wrap` plus `flex: 1 1 19rem` on the
   search does. Wide enough and it is one line; narrow and the search drops
   below the selector on its own. The ONE media query below is not a layout
   breakpoint but a change of KIND: on a phone the selector becomes a chip row
   that scrolls away and only the search line sticks, and the panel becomes the
   bottom sheet the app already uses.

   Everything here is `bt-` prefixed so it cannot collide with style.css or
   with the card rules above. The one exception is `.card-hit`, which IS part
   of a card.
   -------------------------------------------------------------------------- */

/* The page's accent, per legislature — the same content law the card obeys
   ("the theme is the building, posts are the residents"): the CHROME of a feed
   page wears the legislature's colour, and the cards inside it keep their own. */
.feed-page[data-polity="us"] {
  --accent: var(--navy);
  --accent-soft: rgba(10, 49, 97, 0.10);
  --accent-hair: var(--red);
  --accent-wash: var(--wash);
}
.feed-page[data-polity="eu"] {
  --accent: var(--royal);
  --accent-soft: rgba(0, 51, 153, 0.10);
  --accent-hair: var(--gold);
  --accent-wash: var(--wash-eu);
}

.bt-barwrap {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  /* Bleeds into the page's own 1rem gutters so the bar reads as a band rather
     than as a floating box, and so nothing scrolls through its edges. */
  margin: 1.25rem -1rem 0;
  padding: 0.5rem 1rem 0.55rem;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}

.bt-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1 1 19rem;
  min-width: 0;
}

/* --- the legislature selector -------------------------------------------- */

/* Plain links in a pill row: switching legislature is NAVIGATION, so it works
   without JavaScript, it is linkable, the back button is right, and it stores
   nothing (spec §7). */
.bt-seg {
  display: flex;
  gap: 2px;
  background: var(--wash);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 2px;
  flex: none;
}
.bt-seg a {
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  text-decoration: none;
  padding: 0.36rem 0.78rem;
  border-radius: 999px;
  line-height: 1.25;
}
.bt-seg a:hover { color: var(--navy-ink); }
.bt-seg a[aria-current="page"] {
  background: var(--card);
  color: var(--accent);
  box-shadow: 0 1px 2px rgba(10, 49, 97, 0.12);
}

/* Past four legislatures the same slot is a button and a menu (spec §7). */
.bt-picker { position: relative; flex: none; }
.bt-picker-btn { font-weight: 700; }
/* The picker's own menu hangs off the picker button, not off the whole bar —
   restated after the shared rule above, which would otherwise stretch it across
   the bar's full width. */
.bt-menu { position: absolute; top: calc(100% + 0.4rem); left: 0; right: auto; z-index: 25; min-width: 20rem; }
.bt-menu[hidden] { display: none; }
.bt-menu .bt-search { margin-bottom: 0.6rem; }
.bt-menu-list { display: flex; flex-direction: column; }
.bt-menu-list a {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink-soft);
  text-decoration: none;
  padding: 0.34rem 0.2rem;
  border-radius: 6px;
}
.bt-menu-list a:hover { background: var(--wash); color: var(--navy-ink); }
.bt-menu-list a[aria-current="page"] { color: var(--navy-ink); font-weight: 700; }
.bt-menu-list a[hidden] { display: none; }

/* --- the search field ----------------------------------------------------- */

.bt-search {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1 1 19rem;
  min-width: 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 0 0.7rem;
  box-shadow: 0 1px 4px rgba(10, 49, 97, 0.05);
}
.bt-search .glyph { width: 17px; height: 17px; color: var(--ink-faint-aa); }
.bt-search input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  padding: 0.6rem 0;
  font-family: var(--sans);
  font-size: 15px;
  color: var(--ink);
}
.bt-search input::placeholder { color: var(--ink-faint-aa); }
/* THE RING MOVES TO THE BOX, IT IS NOT REMOVED. The old two-pane search killed
   the focus outline outright (`outlineStyle: 'none'`) and put nothing back, so
   a keyboard reader could not tell whether they were in the field. Here the
   input's own outline goes and the WRAPPER draws the ring instead — a border
   in the page's accent plus a 3px soft halo, which is the whole control rather
   than a rectangle inside it. Remove one of these two lines and the other is a
   defect (WCAG 2.2 SC 2.4.7). */
.bt-search input:focus { outline: none; }
.bt-search.is-focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.bt-clear {
  background: none;
  border: 0;
  padding: 6px;
  margin: -6px -4px -6px 0;
  line-height: 0;
  color: var(--ink-faint-aa);
  cursor: pointer;
}
.bt-clear[hidden] { display: none; }
.bt-clear .glyph { width: 15px; height: 15px; }

/* --- the Display button --------------------------------------------------- */

.bt-disp {
  display: flex;
  align-items: center;
  gap: 0.42rem;
  flex: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 0.52rem 0.72rem;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  color: var(--navy);
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(10, 49, 97, 0.05);
}
.bt-disp .glyph { width: 17px; height: 17px; }
.bt-disp .glyph-sm { width: 14px; height: 14px; }
.bt-disp[aria-expanded="true"] { background: var(--wash); border-color: var(--accent); }
/* The state dot is a SIGNAL, never the message: the button's accessible name
   carries the count in words, because a colour is not a sentence. */
.bt-dot { width: 7px; height: 7px; border-radius: 99px; background: var(--accent-hair); flex: none; }
.bt-dot[hidden] { display: none; }

/* --- what drops out of the bar -------------------------------------------- */

/* ⚠️ BOTH OF THESE HANG OFF THE BAR, NOT OFF THE PAGE. The bar is sticky, so a
   panel in the page's flow sits where the page was when it was drawn — open
   Display after scrolling a screen and the button expands onto nothing. They
   are absolutely positioned inside `.bt-barwrap` (which is `position: sticky`
   and therefore their containing block), so they follow the bar. `left`/`right`
   are the bar's own gutter, undoing its bleed so the panel lines up with the
   column rather than with the bleed. */
.bt-drop,
.bt-panel {
  position: absolute;
  top: 100%;
  left: 1rem;
  right: 1rem;
  z-index: 1;
}

/* The resting hint: what the box searches, shown on focus with an empty field. */
.bt-drop {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  box-shadow: 0 6px 24px rgba(10, 49, 97, 0.10);
  padding: 0.85rem 1rem 0.95rem;
  margin: 0.5rem 0 0;
}
.bt-drop[hidden] { display: none; }
.bt-drop .bt-eyebrow { margin-bottom: 0.35rem; }
.bt-drop p {
  font-size: 0.88rem;
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 0;
  max-width: none;
}

.bt-count {
  font-family: var(--sans);
  font-size: 0.84rem;
  color: var(--ink-faint-aa);
  margin: 0.8rem 0 0;
  max-width: none;
}
.bt-count[hidden] { display: none; }

/* The standing "you are looking at a filtered feed" strip. It is a flex child
   of the sticky bar with a full-width basis, so it takes a line of its own
   inside the band and travels with it — see the sticky note at the top of this
   section. The band is one line taller only while something is hidden. */
.bt-filtered {
  flex: 1 0 100%;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
  font-family: var(--sans);
  font-size: 0.82rem;
  color: var(--gold-text);
  background: #FFFDF2;
  border: 1px solid #F0E4B8;
  border-radius: 8px;
  padding: 0.45rem 0.7rem;
  margin: 0.55rem 0 0;
}
.bt-filtered[hidden] { display: none; }
.bt-filtered button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-weight: 700;
  color: var(--navy);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

/* The D25 transposition — the other feeds' matches, with a way to them. */
.bt-elsewhere {
  font-family: var(--sans);
  font-size: 0.86rem;
  line-height: 1.6;
  color: var(--ink-soft);
  border: 1px dashed var(--line);
  border-radius: 10px;
  background: var(--card);
  padding: 0.75rem 0.95rem;
  margin: 1.1rem 0 0;
}
.bt-elsewhere[hidden] { display: none; }
.bt-elsewhere p { margin: 0; max-width: none; }
.bt-elsewhere p + p { margin-top: 0.35rem; }
.bt-elsewhere a { font-weight: 600; }

/* --- the display panel ---------------------------------------------------- */

/* THE PAGE IS HELD STILL ONLY WHERE THE PANEL IS A SHEET (owner rulings:
   locked everywhere 2026-08-20 morning, REVERTED for desktop the same evening
   — "revert to how it originally was"). `dom.ts` `lockScroll` still writes
   `html.bt-locked` from the one chokepoint every close path runs through; the
   class is a STATE, and this stylesheet decides what it costs per surface:

   * On a PHONE the panel is a bottom sheet with a scrim, and a sheet whose
     page scrolls behind it is a broken sheet — the cap lives in the phone
     media query at the end of this file.
   * On a DESKTOP the panel is a dropdown and the class costs NOTHING: the
     page scrolls freely, as it originally did. (The owner's fuller clamp spec
     — scroll to the panel's bottom + a buffer, then stop — sits in the paused
     stash, undiscussed; if it lands it replaces this paragraph.)

   ⚠️ BOUGHT KNOWLEDGE, KEPT FOR THE DAY A DESKTOP CAP RETURNS. The cap goes on
   <html> ONLY — measured live, 2026-08-20, at 1280×720 with the bar stuck:

     html + body hidden → the bar's rect top is -609px. UNSTUCK.
     html hidden        → 0px. sticky holds.
     body hidden        → 0px. sticky holds.

   Overflow PROPAGATION explains it: the viewport takes its overflow from
   <html>, falling back to <body> only when <html> is `visible`; cap both and
   <body> becomes the scroll container, the sticky bar's scrollport is then the
   body box, and the bar (panel with it) drops off the top of the screen. The
   `position: fixed` body pattern is refused for the same reason (`dom.ts`).
   And a DESKTOP cap must pay the scrollbar gutter back (`--bt-scrollbar`,
   still measured by `dom.ts`) or the centred column jumps ~15px sideways at
   open — the payment rule left with the desktop cap and returns with it. A
   phone has no classic scrollbar, so the sheet cap below pays nothing.

   ⚠️ ON /us AND /eu THE DEBT IS PAID AT THE ROOT NOW, AND THE LAST SENTENCE
   WAS HALF TRUE (2026-08-25). `style.css` gained
   `html:has(main.feed-page) { scrollbar-gutter: stable }` — for the refresh
   flicker, not for this — and a stable gutter stays reserved under
   `overflow: hidden`, which is what the cap below is. Measured that day at 1280
   with a classic scrollbar: capping <html> took the centred column 312.4 → 320
   without the rule and 312.4 → 312.4 with it; and at 410px, with the real
   Display panel driven open, `main.feed-page` went 395.2 → 410.4 without and
   395.2 → 395.2 with. So on the feed pages a desktop cap would arrive with its
   payment already made, by CSS rather than by a measured custom property.

   The half that was wrong: "a phone has no classic scrollbar" is a fact about a
   DEVICE, and the cap below lives in a WIDTH query (≤47.9375rem). A desktop
   window dragged narrower than that has a classic scrollbar and did jump when
   the sheet opened — no `padding-right: var(--bt-scrollbar)` rule has ever
   existed in this file to pay it. That is now closed on the feed pages and
   nowhere else, because the root rule is deliberately scoped: reserving a
   gutter on a page that does not overflow leaves the full-bleed header 15px
   short of the window with paper showing through, which an adversarial pass
   caught on /auth/callback, /404 and /account. Nothing here is removed:
   `--bt-scrollbar` is still published by `dom.ts` and is still what a desktop
   cap would use on the pages the root rule does not reach. */

.bt-backdrop { display: none; }

.bt-panel {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  box-shadow: 0 8px 28px rgba(10, 49, 97, 0.12);
  padding: 0.9rem 1.1rem 1rem;
  margin: 0.5rem 0 0;
  /* NO max-height AND NO INTERNAL SCROLL ON DESKTOP — the panel's own
     scrollbar arrived WITH the page lock (a held page could not scroll to
     reveal a tall panel) and left with it (owner, 2026-08-20 night: "desktop
     did not used to have that scroll bar"). With the page scrolling freely
     again, a tall panel is revealed the original way: by scrolling the page.
     The phone's SHEET still carries its own overflow in the media query below
     — a sheet is viewport-anchored and has no page scroll to lean on. The
     `--bt-panel-max` measurement in feed-page.ts is currently UNCONSUMED;
     it stays because the stashed clamp spec (held page, no panel scrollbar —
     the owner's own design) would need it back. */
}
.bt-panel[hidden] { display: none; }
.bt-panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.bt-eyebrow {
  font-family: var(--sans);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gold-text);
  margin: 0;
  max-width: none;
}
.bt-panel-title {
  font-family: var(--serif);
  font-size: 1.18rem;
  font-weight: 650;
  color: var(--navy-ink);
  margin: 0.1rem 0 0.5rem;
  max-width: none;
}
.bt-x {
  background: none;
  border: 0;
  padding: 8px;
  margin: -8px -6px 0 0;
  line-height: 0;
  color: var(--ink-faint-aa);
  cursor: pointer;
}
.bt-x .glyph { width: 17px; height: 17px; }
.bt-group {
  font-family: var(--sans);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-faint-aa);
  margin: 1rem 0 0.1rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--line-soft);
  max-width: none;
}
.bt-group:first-of-type { border-top: 0; padding-top: 0; margin-top: 0.2rem; }
.bt-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--line-soft);
}
.bt-row:last-of-type { border-bottom: 0; }
.bt-row-text { min-width: 0; }
.bt-row-label {
  display: block;
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 500;
  color: var(--ink);
}
.bt-row-cap {
  display: block;
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink-faint-aa);
  margin-top: 1px;
  max-width: none;
}
/* The app's own `rowSuperseded` grammar: a control whose answer is currently
   being decided by something else is dimmed rather than hidden, so the reader
   can see what it would do. */
.bt-row.is-superseded { opacity: 0.45; }
.bt-row.is-disabled .bt-row-label { color: var(--ink-faint-aa); }
.bt-note {
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
  color: var(--gold-text);
  margin: 0.5rem 0 0;
  max-width: none;
}
.bt-keyword-cap { margin: 0.35rem 0 0.6rem; }
.bt-foot {
  font-family: var(--sans);
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--ink-faint-aa);
  margin: 0.8rem 0 0;
  padding-top: 0.7rem;
  border-top: 1px solid var(--line-soft);
  max-width: none;
}

/* A real checkbox, painted as the app's BrandSwitch: it keeps the keyboard
   behaviour, the role and the checked state a switch drawn out of <div>s would
   have to reinvent badly. */
.bt-sw {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  position: relative;
  width: 44px;
  height: 26px;
  border-radius: 999px;
  background: #CFD9E6;
  border: 0;
  cursor: pointer;
  transition: background 0.12s;
}
.bt-sw::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: 999px;
  background: #fff;
  box-shadow: 0 1px 3px rgba(10, 49, 97, 0.35);
  transition: left 0.12s;
}
.bt-sw:checked { background: var(--accent); }
.bt-sw:checked::after { left: 21px; }
.bt-sw:disabled { opacity: 0.5; cursor: default; }

/* The docket keywords: the reader's own words, in this browser. */
.bt-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 0.6rem; }
.bt-chip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--ink);
  background: var(--accent-wash);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.18rem 0.3rem 0.18rem 0.65rem;
}
.bt-chip-x {
  background: none;
  border: 0;
  padding: 3px;
  line-height: 0;
  color: var(--ink-faint-aa);
  cursor: pointer;
}
.bt-chip-x .glyph { width: 13px; height: 13px; }
.bt-addrow { display: flex; gap: 6px; }
.bt-kw {
  flex: 1;
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--card);
  padding: 0.45rem 0.6rem;
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink);
}
.bt-add {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: #fff;
  cursor: pointer;
}
.bt-add .glyph { width: 16px; height: 16px; stroke-width: 2.4; }

/* --- results: the tier dividers and the highlight ------------------------- */

/* Drawn only when more than one group has hits — with one group there is
   nothing to explain, and a divider would be noise. */
.bt-tier {
  font-family: var(--sans);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-faint-aa);
  margin: 0.4rem 0 -0.2rem;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  max-width: none;
}
.bt-tier::after { content: ""; flex: 1; height: 1px; background: var(--line-soft); }
.bt-tier:first-child { margin-top: 0; }

/* A GOLD TINT AND NOTHING ELSE. The record's words are never re-weighted or
   re-coloured to show a match — a highlight says "this is why it is here", not
   "this part matters more". Not `.card.is-highlighted`, which is the deep-link
   class and means something entirely different. */
mark.bt-hit {
  background: rgba(255, 204, 0, 0.45);
  color: inherit;
  border-radius: 2px;
  padding: 0 1px;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* THE DISCLOSURE ROW (spec §4) — and the two selectors below are the whole of
   its logic. It is the immediate next sibling of `.card-body`, so:
     - a body the browser did NOT have to clip carries no `data-clipped`, and a
       bodyLines match is therefore already on screen: the row collapses;
     - an OPEN card shows its summary and its whole body, so the row collapses
       there too.
   What is left is exactly the case the row exists for: a match the reader
   cannot see, on a card that looks like a false positive without it. No
   JavaScript decides any of this — the one thing only a browser knows (did
   this body overflow?) is already written on the element by `dom.ts`. */
.card-hit {
  font-family: var(--sans);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink-soft);
  margin: 0.55rem 0 0;
  padding-left: 0.7rem;
  border-left: 2px solid var(--accent-hair, var(--line));
  max-width: none;
}
.card-hit .lead { font-weight: 700; color: var(--ink); }
.card-body:not([data-clipped="1"]) + .card-hit[data-field="bodyLines"] { display: none; }
.card.is-open .card-hit { display: none; }

/* --- the phone ------------------------------------------------------------ */

/* NOT A LAYOUT BREAKPOINT — a change of kind. On a phone two bands of sticky
   chrome would eat the screen, so the selector becomes a chip row that scrolls
   away with the page and only the search line stays. */
@media (max-width: 47.9375rem) {
  /* ⚠️ THE WRAP STAYS THE STICKY ELEMENT, PULLED UP BY THE CHIP ROW'S HEIGHT.
     The obvious version — leave the wrap static and stick `.bt-bar` inside it —
     does not work and looks like it does: a sticky element can only travel
     inside its own PARENT's box, and the wrap's box ends a few pixels below the
     bar, so the whole thing scrolled away on the first real phone test
     (2026-08-20). Sticking the wrap with a NEGATIVE top of exactly the chip
     row's height parks the chip row above the viewport and lands the search
     line at 0, which is the spec's phone rule: the selector scrolls away, only
     the search sticks. `min-height` rather than `height` so the failure is
     graceful — a chip row that grows leaves a sliver of itself visible instead
     of clipping. */
  .bt-barwrap {
    --bt-chiprow-h: 2.4rem;
    top: calc(-1 * var(--bt-chiprow-h));
    display: block;
    padding-bottom: 0;
  }
  .bt-seg {
    box-sizing: border-box;
    min-height: var(--bt-chiprow-h);
    display: flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: 0;
    border-radius: 0;
    padding: 0.15rem 1rem 0.5rem;
    margin: 0 -1rem;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .bt-seg::-webkit-scrollbar { display: none; }
  .bt-seg a {
    flex: none;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 0.3rem 0.7rem;
  }
  .bt-seg a[aria-current="page"] {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    box-shadow: none;
  }
  .bt-bar {
    margin: 0 -1rem;
    padding: 0.4rem 1rem 0.5rem;
  }
  .bt-picker { width: 100%; }
  .bt-menu { min-width: 0; right: 0; }

  /* The panel becomes the app's bottom sheet. The backdrop is a real element
     rather than a pseudo one so that tapping it closes the sheet — a scrim you
     cannot dismiss by tapping is a scrim that reads as a broken modal. */
  .bt-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 40;
    background: rgba(14, 42, 78, 0.38);
  }
  .bt-backdrop[hidden] { display: none; }
  .bt-panel {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 41;
    margin: 0;
    /* A sheet is anchored to the VIEWPORT, not to the bar, so it keeps its own
       height rather than the measured `--bt-panel-max` the dropdown uses. This
       rule sits after the base one and wins. */
    max-height: 82vh;
    overflow-y: auto;
    /* A flick that runs out of sheet must not carry on into the locked page
       behind it — the one way it could still appear to move. */
    overscroll-behavior: contain;
    border: 0;
    border-radius: 14px 14px 0 0;
    box-shadow: 0 -6px 24px rgba(10, 49, 97, 0.18);
    padding: 1rem 1.1rem calc(1.2rem + env(safe-area-inset-bottom));
  }
  /* The sheet holds the page still — a sheet whose page scrolls behind its
     scrim is a broken sheet. PHONE-ONLY since the owner's desktop revert
     (2026-08-20 evening); the why-html-only measurements live at the class's
     main comment in the display-panel section. */
  html.bt-locked { overflow: hidden; }
}
