/* ============================================================
   FEC Watchlist -- "glass console" design tokens
   Dark neon-street palette: cyan for structure, yellow for action,
   red reserved for attention. Restrained motion.
   ============================================================ */

:root {
  /* ground truth palette (from the sidebar mark; --bg darkened ~8% in
     lightness from the original #0b1a1e, hue held) */
  --bg: #0a181c;
  --primary: #36e0c8;   /* cyan: data + structure */
  --accent: #fcee0a;    /* yellow: primary action + active state */
  --alert: #ff003c;     /* red: attention only, never decoration -- too dark for AA text contrast on its own, see --alert-text */
  --alert-text: #ff5e83; /* same red family, lightened for body-size text on dark rows. Measured against the actual composited IE-row background (#182f31, sampled from a real render, not the flat --bg token): the old #ff4d76 read 4.41:1 -- just under AA's 4.5:1 for small text -- so it was raised to #ff5e83, which measures 4.82:1 there and 5.32:1 against --surface-1 (the pair-detail expanded-row background). --alert itself (#ff003c) only clears ~3.6:1 and must never carry small text directly. */

  /* derived surfaces: two lighter tints of --bg, same blend ratios as
     before so bg-to-surface contrast is unchanged */
  --surface-1: #192629;
  --surface-2: #253135;

  /* derived text: near-white + dimmed, off the palette */
  --text: #f3fdfc;
  --heading: #ffffff;
  --text-dim: #929e9f;

  /* derived low-alpha washes (documented derivations, not new hues) */
  --border: rgba(54, 224, 200, 0.18);        /* muted cyan: borders/dividers */
  --border-strong: rgba(54, 224, 200, 0.32); /* panel top highlight */
  --primary-glow: rgba(54, 224, 200, 0.35);
  --primary-hover-tint: rgba(54, 224, 200, 0.08);
  --primary-border-soft: rgba(54, 224, 200, 0.5);
  --accent-glow: rgba(252, 238, 10, 0.35);
  --accent-hover-tint: rgba(252, 238, 10, 0.08);
  --alert-wash: rgba(255, 0, 60, 0.14);
  /* How much the sidebar title is stretched vertically. The title is fitted
     to a width (see fitWordmark in app.js) and then stretched from that,
     so this is the only thing that decides its height. Declared here
     because three places need to agree on it: the title's transform, the
     emoji's counter-transform, and app.js -- which both sizes the emoji to
     the stretched capitals and gives the block back the height the
     transform spills. */
  --title-stretch: 1.15;

  /* The status ring's unfilled track: --primary at low alpha, so it reads
     as dark teal against --panel-bg without introducing a new hue. */
  --ring-track: rgba(54, 224, 200, 0.2);
  /* The one green in the palette, sampled from the FEC Watchlist logo's
     own disc (#62d765/#58d45d in the artwork) rather than invented: it
     marks a committee as live in the sidebar and nothing else. 9.7:1 on
     --bg, so it carries small text. */
  --live: #5ad45e;
  /* The AI notice's veil: plain black at 72%, the one place the app dims
     the whole page. */
  --veil: rgba(0, 0, 0, 0.72);
  --on-accent: var(--bg);   /* dark text on a yellow fill */

  /* legacy aliases so every rule below reads by role, not by hue */
  --ground: var(--bg);
  --panel-bg: color-mix(in srgb, var(--surface-1) 82%, transparent);
  --panel-border: var(--border);
  --panel-highlight: var(--border-strong);
  --panel-blur: 14px;
  --panel-radius: 8px;
  --panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  --secondary: var(--text-dim);
  --panel-label: var(--primary);

  /* WIRED wordmark palette -- same source palette, kept as its own set so a
     future logo-only tweak never has to touch the site tokens above. */
  --wired-bg: var(--bg);
  --wired-primary: var(--primary);
  --wired-shadow: var(--alert);
  --wired-panel: var(--accent);

  /* layout */
  --nav-width: 232px;
  --nav-collapsed-width: 56px;
  /* The mark's rendered width when expanded (sidebar width less
     .wired-mark-wrap's 16px side padding) and, from it, the rendered width
     of one of its five 120-unit tiles on the 648-unit viewBox. The collapsed
     rail clips the mark to one tile of exactly this size -- it never scales
     the mark down. */
  --wired-logo-w: calc(var(--nav-width) - 32px);
  --wired-tile: calc(var(--wired-logo-w) * 120 / 648);

  /* Expanded sidebar layout. The WIRED mark's rendered width is the
     sidebar's content width, and --sb-pad is the inset inside the fixed
     --nav-width that produces it -- so the mark's left and right edges are
     the edges everything else aligns to. --sb-bleed is the only thing that
     crosses them: how far a menu row's highlight extends past both. */
  --sb-content-w: var(--wired-logo-w);
  --sb-pad: 16px;
  --sb-bleed: 12px;
  /* One spacing scale. Groups are separated by --sb-gap-group; everything
     inside a group uses the smaller steps. */
  --sb-gap-group: 32px;
  --sb-gap-logo-title: 16px;
  --sb-gap-head-divider: 8px;
  --sb-gap-divider-item: 12px;
  --sb-gap-list-item: 8px;
  --sb-gap-above-latest: 16px;
  --sb-row-h: 36px;

  /* Sidebar toggle button: size of its hit area, its distance above the
     viewport bottom, and the minimum clear space between it and the
     sidebar's own content. The three add up to what #nav-scroll reserves
     at the bottom of its scrolling area. */
  --nav-btn-size: 36px;
  --nav-btn-bottom: 45px;
  --nav-btn-clear: 16px;
  --nav-btn-reserve: calc(var(--nav-btn-bottom) + var(--nav-btn-size) + var(--nav-btn-clear));
  --content-max: 1440px;
  --row-height: 40px;

  /* motion */
  --motion-fast: 140ms;
  --nav-motion: 200ms;
  /* One curve for every part of the sidebar collapse -- width, both fades,
     the menu's vertical travel, the logo's slide, the icon scale and the
     toggle glyph's rotation all use it, so the whole thing reads as one
     movement. Decelerating: ~90% of the travel is done by 45% of the
     duration, then it settles, which is what stops the width animation from
     looking like it arrives after the fades. */
  --nav-ease: cubic-bezier(0.2, 0, 0, 1);
  /* The crossfade. Each side gets 60% of the timeline and they overlap in
     the middle 20%: what leaves goes out over [0%, 60%], what arrives comes
     in over [40%, 100%]. Over that overlap the product of the two opacities
     peaks at ~0.014, so the two are never both legible, but neither side is
     idle for half the animation the way a strict two-halves split leaves
     them. Both windows end no later than --nav-motion. */
  --nav-fade-dur: calc(var(--nav-motion) * 0.6);
  --nav-fade-delay: calc(var(--nav-motion) * 0.4);
  /* How far .nav-body travels up when the rail closes: everything the
     wordmark block occupies above it, its 78px bottom margin included.
     Measured by app.js from the live layout (the title's font-size is
     fitted there) and written back onto .nav; this is the pre-JS
     fallback, so it only has to be close enough not to flash. */
  --nav-shift: 119px;
}

@media (prefers-reduced-motion: reduce) {
  /* 1ms, not 0: a 0ms transition never fires `transitionend`, and JS below
     (hiding collapsed-only content, re-fitting the wordmark) depends on
     that event to run. 1ms is imperceptible but still a real transition. */
  :root { --motion-fast: 1ms; --nav-motion: 1ms; }
}

* { box-sizing: border-box; }

/* Root font size: was the browser default (16px, implicit -- never set
   explicitly before); raised by 1px to 17px. Every size below is in rem or
   em, so this is the one place the whole type scale is set. */
html { background: var(--bg); font-size: 17px; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'IBM Plex Sans', sans-serif;
  font-size: 0.875rem;
  min-height: 100vh;
  position: relative;
}

/* Fixed background layer: a 45-degree cyan hatch at ~4% alpha (8px period)
   plus a soft vignette darkening the corners. Static -- nothing in this
   layer moves. Sits behind all content and never touches text contrast. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(ellipse 140% 140% at 50% 50%, transparent 55%, rgba(0, 0, 0, 0.35) 100%),
    repeating-linear-gradient(45deg, rgba(54, 224, 200, 0.04) 0px, rgba(54, 224, 200, 0.04) 1px, transparent 1px, transparent 8px);
}

/* A single-pixel scanline texture over the whole page, ~3% alpha, static.
   No animation, no flicker -- purely a surface texture. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;
  background: repeating-linear-gradient(0deg, rgba(54, 224, 200, 0.03) 0px, rgba(54, 224, 200, 0.03) 1px, transparent 1px, transparent 2px);
}

/* The sidebar title is in this list rather than styling itself: it is the
   same headline face as every page H1 (INBOX, MONEY OUT ...), and listing
   it here is what keeps the two in sync -- a change to the headline font,
   weight or colour reaches both. Its size and position are its own (see
   .wordmark-title and fitWordmark in app.js); nothing else is. */
h1, h2, h3, .figure, .wordmark, .wordmark-title, .page-title {
  font-family: 'Archivo Narrow', sans-serif;
  font-weight: 700;
  color: var(--heading);
  margin: 0;
}

.mono, .amount, .id, code, .micro { font-family: 'IBM Plex Mono', monospace; }

/* numeric columns: tabular figures, right aligned, header matches */
.num, td.num, th.num { font-variant-numeric: tabular-nums; text-align: right; }

a { color: var(--text); text-decoration: none; }
a:hover { text-decoration: underline; text-decoration-color: var(--primary); text-underline-offset: 2px; }
a.amount, a.name { color: var(--heading); }
a.amount:hover, a.name:hover { color: var(--primary); text-decoration-color: var(--primary); }

/* ---------------------------------------------------------- focus + motion */

a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible, [tabindex]:focus-visible, tr.row-link:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

.transition, .chip, .navlink, button, .row-link, .pair-row, a {
  transition: background-color var(--motion-fast) ease, border-color var(--motion-fast) ease,
              color var(--motion-fast) ease, box-shadow var(--motion-fast) ease,
              opacity var(--motion-fast) ease;
}

/* ---------------------------------------------------------------- layout */

.layout { display: flex; min-height: 100vh; max-width: var(--content-max); margin: 0 auto; }

/* .nav is the tab's positioned ancestor (position: sticky) and must never
   clip it: overflow: visible, no transform/will-change here or on .layout.
   It does not scroll itself -- #nav-scroll (a sibling of the toggle button,
   not its parent) does that, so the toggle sits outside any clipping/auto
   overflow context. z-index 20 keeps it above .main; popovers/modals in
   this app use higher z-indices (the user picker at z-index 50, tooltip
   layer above that) so it stays below those. */
.nav {
  width: var(--nav-width);
  flex: 0 0 var(--nav-width);
  position: sticky;
  top: 0;
  align-self: flex-start;
  height: 100dvh;
  overflow: visible;
  z-index: 20;
  display: flex;
  flex-direction: column;
  transition: width var(--nav-motion) var(--nav-ease), flex-basis var(--nav-motion) var(--nav-ease);
}
/* Set for one frame on page load (removed by app.js after the first paint)
   so the collapsed state applied from localStorage never animates in. */
html.no-nav-transition .nav,
html.no-nav-transition .nav * { transition: none !important; }

/* The clipping window. It is the flex child that narrows with the sidebar;
   contain: layout paint keeps the sidebar's own relayout and repaint off
   the rest of the page during the width animation. */
.nav-clip {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  contain: layout paint;
  /* The clip stops short of the bottom-anchored toggle button rather than
     merely padding content away from it, so the Activity box and LATEST
     list clear the button at every window height and every scroll
     position, not just when scrolled to the end. --nav-btn-clear is the
     gap that leaves. See .nav-toggle-btn. */
  margin-bottom: var(--nav-btn-reserve);
}

#nav-scroll {
  /* Fixed at the expanded width in both states -- never the rail's width.
     Everything inside therefore lays out once, at one width, and the
     collapse is a pure clip: no rewrapping "14d" onto its own line, no
     ellipsis appearing in the sidebar title or the LATEST names, no squeezed
     heat strip. The scrollbar, when there is one, sits at the far right of
     this box and is clipped away with everything else. */
  width: var(--nav-width);
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  /* The side padding is what makes the content box exactly the mark's
     width: everything in here is flush with the mark's edges unless it
     deliberately bleeds past them (see .nav a.navlink). */
  padding: var(--sb-pad) var(--sb-pad) 0 var(--sb-pad);
}

/* A flex column rather than a plain block, so this block's own bottom
   margin cannot collapse into a child's and the gap below the title is
   exactly the group gap. Nothing else in the sidebar moves relative to
   anything else: the menu, the Committees box, the Activity box and LATEST
   all sit below this block, so they shift together, and the collapsed
   rail is unaffected -- .nav-body's collapsed offset is measured from this
   block's own top (see measureShift in app.js). */
.nav .wordmark {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  /* No padding of its own: the scroller's inset already puts this on the
     mark's edges. The 16px above comes from the mark's own margin-bottom,
     and this margin is the group gap down to the menu. */
  padding: 0;
  margin-bottom: var(--sb-gap-group);
}

/* The collapsed rail's mark: the FEC Watchlist logo, in the same box the
   WIRED W tile used to occupy so the rail's geometry is unchanged. Out of
   flow and a child of .nav (see base.html), so the narrowing clip never
   touches it and it holds still while the rail closes around it -- it only
   cross-fades, on the arriving half of the same timeline the menu icons
   use, opposite the wordmark it replaces.

   The image is the source JPEG cropped 15% (centre 85% of each side) and
   saved at 256px; cover on a square box, so the crop is all that decides
   the framing. Yellow border, as the WIRED tile had a stroked one. */
.nav-mark {
  position: absolute;
  top: 16px;
  left: calc(var(--nav-collapsed-width) / 2);
  transform: translateX(-50%);
  width: var(--wired-tile);
  height: var(--wired-tile);
  border: 1px solid var(--accent);
  background: #000 url('/static/img/fec-watchlist-logo.png') center / cover no-repeat;
  opacity: 0;
  pointer-events: none;
  z-index: 4;
  transition: opacity var(--nav-fade-dur) var(--nav-ease) 0ms;
}
.nav.collapsed .nav-mark {
  opacity: 1;
  transition-delay: var(--nav-fade-delay);
}

/* The WIRED mark, back at the top of the sidebar above the FEC WATCHLIST
   block. It renders at --wired-logo-w, the sidebar's width less this
   block's 16px of padding on each side, which is also the width the title
   below it is fitted to. Collapsed it fades out over the leaving half of
   the shared timeline while .nav-mark fades in at the same place. */
.wired-mark-wrap {
  padding: 0;
  margin-bottom: var(--sb-gap-logo-title);
  transition: opacity var(--nav-fade-dur) var(--nav-ease) var(--nav-fade-delay);
}
.wired-mark-wrap .wired-mark { width: 100%; }
.nav.collapsed .wired-mark-wrap { opacity: 0; transition-delay: 0ms; }
.wired-mark-wrap:hover .wired-mark { filter: drop-shadow(0 0 10px var(--primary-glow)); }

/* overflow: visible so the outer half of the outline on the first and last
   tiles -- a 1px non-scaling stroke centred on the viewBox edge -- still
   paints instead of being clipped by the svg's own viewport.
   crispEdges because the mark is pixel art: without it a 1px stroke that
   lands between two device pixels is split across both and each half reads
   as a grey blur rather than a line. */
.wired-mark {
  display: block; width: 100%; overflow: visible;
  shape-rendering: crispEdges;
  transition: filter var(--motion-fast) ease;
}

/* Line 1's font-size is computed in app.js from the rendered logo's own
   width (canvas text measurement, redone after fonts load and on resize) --
   the fixed size here is only a pre-JS fallback so there's no flash of
   unsized text. max-width on both lines is also set in JS to the same
   measured logo width, so they can never run wider than the logo even if
   this CSS's padding ever drifts from wired-mark-wrap's. */
/* The stretch. A transform, so it changes no layout: it spills its extra
   height past the bottom of the box, and app.js gives that back as margin
   so the gap down to the menu is measured from the stretched bottom.
   transform-origin: left top means the title grows downward from its own
   top edge, so the gap above it -- to the WIRED mark -- is untouched, and
   the left edge it is flush with cannot move. */
.wordmark-title-wrap {
  transform: scaleY(var(--title-stretch));
  transform-origin: left top;
}
/* Shrink-wraps the title, so the absolutely-positioned emoji is placed
   against the letters' own box. Flush left on the mark's edge; app.js fits
   the text to a share of the mark's width, so what is left over is all on
   the right. */
.wordmark-title-stack {
  position: relative;
  display: block;
  width: fit-content;
}
/* Font family, weight and colour come from the shared heading rule above.
   Only what is specific to this one heading lives here: all caps, one line,
   and a font-size that app.js overwrites with the fitted value (the value
   here is just a pre-JS fallback so there is no flash of unsized text). */
.wordmark-title {
  display: block; max-width: 100%;
  text-transform: uppercase;
  white-space: nowrap; line-height: 1.2;
  font-size: 1.0625rem;
}
/* The heading's emoji. Out of flow so the heading's box -- and everything
   below it -- is unaffected; app.js sets its font-size, left and top from
   measured ink, sized to the letters' stretched capitals and centred on
   them. The counter-scale undoes the wrapper's stretch, so the glyph keeps
   its own proportions while the letters stay stretched, and it keeps its
   own colour rather than inheriting the heading's. */
.wordmark-emoji {
  position: absolute;
  top: 0; left: 0;
  transform: scaleY(calc(1 / var(--title-stretch)));
  transform-origin: 0 0;
  line-height: 1;
  color: initial;
  pointer-events: none;
}

.nav a.navlink {
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* Positioned ancestor for the two out-of-flow children below: the
     collapsed-rail icon and the count. Both are pinned to an x measured
     from this box's left edge, which never moves, so neither slides while
     the sidebar's width animates. */
  position: relative;
  /* The row's highlight is the only thing that crosses the content edges:
     it bleeds --sb-bleed past both, and the padding brings the label and
     the count straight back onto them. The 2px border-left is part of that
     bleed, which is what puts the active row's yellow rule --sb-bleed to
     the left of the text edge. */
  margin: 0 calc(-1 * var(--sb-bleed));
  padding: 0 var(--sb-bleed) 0 calc(var(--sb-bleed) - 2px);
  height: var(--sb-row-h);
  color: var(--text);
  font-size: 0.8125rem;
  border-left: 2px solid transparent;
  border-radius: 0 4px 4px 0;
}
.nav a.navlink:hover { background: var(--primary-hover-tint); text-decoration: none; }
.nav a.navlink.active {
  color: var(--accent);
  border-left: 2px solid var(--accent);
  background: var(--accent-hover-tint);
  box-shadow: inset 0 0 12px -6px var(--accent-glow);
}
/* In flow, at the end of the row's space-between: the row's box is a fixed
   width inside #nav-scroll, which never reflows while the rail narrows, so
   the count holds its x on the content's right edge without needing to be
   taken out of flow for it. */
.nav .navcount {
  flex: none;
  display: flex; align-items: center;
  color: var(--text-dim); font-family: 'IBM Plex Mono', monospace; font-size: 0.75rem;
}
.nav .navcount.hot { color: var(--accent); }

.main { flex: 1; display: flex; min-width: 0; }

/* ------------------------------------------------------- collapsible nav */

/* Both the handle and the expand button are children of .nav (not of
   #nav-scroll), so #nav-scroll's own overflow-y: auto never clips them --
   and since .nav itself is overflow: visible, nothing in this sidebar
   clips them at all. */

/* A hairline down the sidebar's own right edge, plus an invisible 8px
   drag strip centred on it. touch-action/user-select are set here (not in
   JS) so they're in effect before the very first pointerdown. */
.nav-handle {
  position: absolute; top: 0; bottom: 0; right: -4px; width: 8px;
  z-index: 6;
  cursor: col-resize;
  touch-action: none;
  user-select: none;
  background: transparent;
}
.nav-handle::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 1px;
  transform: translateX(-50%);
  background: var(--border);
  transition: background-color var(--motion-fast) ease;
}
.nav-handle:hover::before { background: var(--primary); }
.nav-handle:focus-visible { outline: none; }
.nav-handle:focus-visible::before { background: var(--primary); box-shadow: 0 0 0 2px var(--primary); }
/* Set on <body> for the duration of a drag (see app.js) -- yellow line,
   page-wide col-resize cursor, and .main's hover styles suppressed so
   nothing but the handle itself reacts while dragging. */
body[data-nav-dragging] .nav-handle::before { background: var(--accent); }
body[data-nav-dragging] { cursor: col-resize; }
body[data-nav-dragging] .main { pointer-events: none; }
/* Blocks in the main column whose content re-wraps when the column's width
   changes (see prepareReflow in app.js). Both classes exist only while a
   toggle is running; at rest these elements carry nothing but their
   data-nav-reflow attribute, which styles nothing. Same duration and curve
   as the sidebar itself, so the block's height and the chips inside it
   settle on the frame the rail does. */
.nav-reflow-box {
  transition: height var(--nav-motion) var(--nav-ease);
  will-change: height;
}
.nav-reflow-item {
  transition: transform var(--nav-motion) var(--nav-ease);
  will-change: transform;
}
/* Text blocks whose height is mid-animation: see prepareReflow. clip, not
   hidden -- it never creates a scroll container, so nothing about the
   block's own layout or any sticky descendant changes for the 200ms it is
   on. */
.nav-reflow-clip { overflow: clip; }

/* Only while something is actually moving -- set at the start of the
   transition and dropped on transitionend, never left on the elements (a
   permanent will-change keeps the layers alive and costs memory for the
   whole session). */
body[data-nav-moving] .nav-body { will-change: transform; }
body[data-nav-moving] .nav-fade { will-change: opacity; }
body[data-nav-moving] .navlink-icon { will-change: transform, opacity; }
body[data-nav-moving] .nav-mark,
body[data-nav-moving] .wired-mark-wrap { will-change: opacity; }

/* Toggle button: present in both states, anchored to the sidebar's own
   right edge -- half a rail width in from it, so it is centred in the
   collapsed rail and sits near the bottom-right corner of the open
   sidebar, travelling with the edge as the width animates. That is pure
   CSS: `right` is resolved against .nav's box, so the button follows the
   width transition (and a live drag) exactly, with nothing to jump at
   either end. Vertically it is a fixed distance up from the bottom of
   .nav, which is sticky and 100dvh tall, so up from the bottom of the
   viewport. Neither anchor is flow-measured, so the sidebar's own content
   can never displace it. .nav is overflow: visible, so nothing clips it. */
.nav-toggle-btn {
  position: absolute;
  right: calc(var(--nav-collapsed-width) / 2);
  transform: translateX(50%);
  bottom: var(--nav-btn-bottom);
  z-index: 5;
  width: var(--nav-btn-size); height: var(--nav-btn-size);
  display: flex; align-items: center; justify-content: center;
  border: none; background: none; padding: 0;
  border-radius: 4px;
  color: var(--text-dim);
  cursor: pointer;
}
.nav-toggle-btn:hover { color: var(--primary); }
.nav-toggle-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* Only the icon turns, never the button: the hit area and the focus ring
   stay put. Expanded is the rotated state -- the chevron points back at the
   sidebar edge, the way it will close -- and it turns over the same 200ms
   the sidebar itself takes. */
.nav-toggle-btn svg {
  display: block;
  transform: rotate(180deg);
  transition: transform var(--nav-motion) var(--nav-ease);
}
.nav.collapsed .nav-toggle-btn svg { transform: rotate(0deg); }

.nav.collapsed { width: var(--nav-collapsed-width); flex-basis: var(--nav-collapsed-width); }
/* .nav-body is the whole sidebar below the wordmark: the menu and the
   Activity box. Collapsed it sits --nav-shift higher -- the height the
   wordmark block occupies -- so the icon column ends up directly under the
   W tile with the same 12px gap the wordmark had. This is a transform on
   the shared timeline, so the menu *travels* there; nothing about the
   sidebar's flow height differs between the two states, which is what used
   to make this a 71px jump at the end of the width animation. */
.nav-body { transition: transform var(--nav-motion) var(--nav-ease); }
/* Set by app.js for one flush when --nav-shift itself is re-measured (the
   wordmark's font arriving, a zoom change), so that correction lands
   instantly instead of sliding the menu. */
.nav-body.nav-no-motion { transition: none; }
.nav.collapsed .nav-body { transform: translateY(calc(-1 * var(--nav-shift))); }

/* Collapsed-only content fades via opacity, and once the fade has finished
   app.js adds .nav-gone + inert (see the nav's `transitionend` listener):
   visibility: hidden, not [hidden]/display: none, so taking it out of the
   tab order and off screen does not change the sidebar's flow height and
   cannot shift .nav-body under the transform above. */
.nav-gone { visibility: hidden; }
/* Labels, counts, the title, Activity and LATEST all carry .nav-fade.
   Expanding, they are what arrives, so they take the late window
   ([40%, 100%]) while the icons leave over the early one; collapsing, they
   are what leaves, so the windows swap. They overlap over the middle 20%,
   where the product of the two opacities stays under 0.02 -- a hand-off,
   not a dissolve in which both are legible. */
.nav-fade { opacity: 1; transition: opacity var(--nav-fade-dur) var(--nav-ease) var(--nav-fade-delay); }
.nav.collapsed .nav-fade { opacity: 0; transition-delay: 0ms; }
/* Collapsed logo: the single W tile, at its normal tile size, centred in
   the rail at the same top as the full mark. The full wordmark fades out
   (and, being wider than the rail, is clipped away by #nav-scroll as the
   rail closes); the tile it leaves behind is a whole element, not a
   surviving fragment of it. */

/* Menu icons live only in the collapsed rail, but they are in the DOM in
   both states: out of flow, pinned to the rail's centre line.

   left is measured from the row's padding box, which starts 2px in (the
   always-present border-left), hence the -2px: the 36px box lands on
   0..56 centred on 28, the rail's centre. That puts the 18px glyph on
   19..37, i.e. its left edge one pixel off where the label's first
   character starts (18px) -- the icon's rail position and its label's
   position are the same x, so the glyph needs no horizontal travel to
   appear to take the label's place. Its travel is the vertical one it
   inherits from .nav-body plus the scale below, on the shared timeline. */
.navlink-icon {
  position: absolute;
  top: 0;
  /* Centred on the rail's centre line, measured from the row's padding
     box -- which now starts --sb-pad - --sb-bleed + 2px in from the
     sidebar's left edge. */
  left: calc(var(--nav-collapsed-width) / 2 - var(--sb-pad) + var(--sb-bleed) - 2px);
  /* 0.92 while absent, 1 when arrived: the glyph settles into place as it
     fades in instead of appearing at full size. */
  transform: translateX(-50%) scale(0.92);
  width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 4px;
  color: var(--text-dim);
  opacity: 0;
  pointer-events: none;
  /* Opacity and scale are the crossfade's arriving/leaving side (see
     .nav-fade); colour and fill keep the ordinary hover speed, and neither
     runs during the sidebar transition -- only on hover and on the active
     row. Expanded, the icons are what leaves, so they take the early
     window. */
  transition: opacity var(--nav-fade-dur) var(--nav-ease) 0ms,
              transform var(--nav-fade-dur) var(--nav-ease) 0ms,
              color var(--motion-fast) ease,
              background-color var(--motion-fast) ease, box-shadow var(--motion-fast) ease;
}
/* Full shorthand, not transition-delay: a bare delay would apply to every
   property listed above, putting a lag on the hover colour too. */
.nav.collapsed .navlink-icon {
  opacity: 1;
  transform: translateX(-50%) scale(1);
  transition: opacity var(--nav-fade-dur) var(--nav-ease) var(--nav-fade-delay),
              transform var(--nav-fade-dur) var(--nav-ease) var(--nav-fade-delay),
              color var(--motion-fast) ease,
              background-color var(--motion-fast) ease, box-shadow var(--motion-fast) ease;
}
.navlink-icon svg { display: block; }

/* ---- live drag ----------------------------------------------------------
   Last in this section on purpose: these have the same specificity as the
   .nav.collapsed rules above and must win over them while a drag is live.

   No transitions anywhere in the sidebar during a drag -- app.js drives
   every part directly, frame by frame, from the pointer's own position
   (--nav-p, 0 open .. 1 closed, plus the two opacity ramps it derives from
   it): the labels fade, the icons fade and scale, the menu rides up and the
   logo and toggle glyph turn exactly in step with the edge under the
   finger. The transitions re-engage the moment the drag ends, so the snap
   runs the same 200ms on the same curve from wherever the drag left off. */
body[data-nav-dragging] .nav,
body[data-nav-dragging] .nav-body,
body[data-nav-dragging] .nav-fade,
body[data-nav-dragging] .navlink-icon,
body[data-nav-dragging] .nav-mark,
body[data-nav-dragging] .wired-mark-wrap,
body[data-nav-dragging] .nav-toggle-btn svg { transition: none; }
body[data-nav-dragging] .nav-body { transform: translateY(calc(-1 * var(--nav-p) * var(--nav-shift))); }
body[data-nav-dragging] .nav-fade,
body[data-nav-dragging] .wired-mark-wrap { opacity: var(--nav-label-o); }
body[data-nav-dragging] .navlink-icon,
body[data-nav-dragging] .nav-mark { opacity: var(--nav-icon-o); }
body[data-nav-dragging] .navlink-icon { transform: translateX(-50%) scale(calc(0.92 + 0.08 * var(--nav-icon-o))); }
body[data-nav-dragging] .nav-toggle-btn svg { transform: rotate(calc(180deg - var(--nav-p) * 180deg)); }

.nav a.navlink:hover .navlink-icon { color: var(--text); background-color: var(--primary-hover-tint); }
.nav a.navlink.active .navlink-icon { color: var(--accent); }
/* Collapsed active state: the highlight is the 36x36 box around the icon
   plus the border-left already on the row, and nothing else -- the row's
   own full-width fill fades out as this one fades in, so the rail never
   shows a filled band with no icon in it. */
.nav.collapsed a.navlink.active .navlink-icon {
  background-color: var(--accent-hover-tint);
  box-shadow: inset 0 0 12px -6px var(--accent-glow);
}
.nav.collapsed a.navlink.active { background-color: transparent; box-shadow: none; }
.nav.collapsed a.navlink:hover { background-color: transparent; }

.navlink { display: flex; align-items: center; overflow: hidden; min-width: 0; }
.navlink-text { white-space: nowrap; }
.content { flex: 1; padding: 16px 24px 40px 24px; min-width: 0; max-width: 100%; }
.rail { width: 300px; flex: 0 0 300px; padding: 16px 16px 40px 16px; }

/* -------------------------------------------------------------- panels */

.panel {
  background: var(--panel-bg);
  backdrop-filter: blur(var(--panel-blur));
  -webkit-backdrop-filter: blur(var(--panel-blur));
  border: 1px solid var(--panel-border);
  border-radius: var(--panel-radius);
  box-shadow: inset 0 1px 0 var(--panel-highlight), var(--panel-shadow);
  padding: 14px 16px;
  margin-bottom: 16px;
}
.panel.tight { padding: 10px 14px; }
.panel.no-pad { padding: 0; overflow: hidden; }

/* Main-content cards and panels only (not sidebar, not the right rail):
   clipped corners top-right and bottom-left, 10px notch. `clip-path` on an
   element clips that element's *entire subtree*, so a pseudo-element can
   never draw outside it (verified directly: a ::before extending 1px past
   its clipped parent via inset:-1px simply vanishes, clipped away same as
   real content would be) -- meaning a border can't be added by drawing
   something bigger behind the panel. Inverted instead: the panel's own
   background IS the border color, clipped to the full 10px notch; a
   ::before inset 1px in, clipped to a 9px notch, paints the real glass
   surface on top of it (real DOM content -- panel-head, tables, etc. --
   still paints after ::before, still clipped correctly by the panel's own
   10px clip-path). What's left uncovered is a continuous 1px ring in the
   border color, notch corners included. */
.content .panel, .content .inbox-card {
  position: relative;
  z-index: 0;
  clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px));
  border-radius: 0;
  border: none;
  background: var(--border);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.content .panel::before, .content .inbox-card::before {
  content: "";
  position: absolute;
  inset: 1px;
  /* Without an explicit z-index, an absolutely-positioned pseudo-element
     paints in the "positioned descendants, z-index:auto" stacking stage,
     which comes AFTER normal in-flow content (chips, headlines, tags) --
     not before it. That silently put this translucent, blurred surface
     layer on top of every real content, dimming it. -1 moves it back to
     the negative-z-index stage, behind in-flow content, in front of only
     the panel's own background (the border ring). */
  z-index: -1;
  pointer-events: none;
  background: var(--panel-bg);
  backdrop-filter: blur(var(--panel-blur));
  -webkit-backdrop-filter: blur(var(--panel-blur));
  clip-path: polygon(0 0, calc(100% - 9px) 0, 100% 9px, 100% 100%, 9px 100%, 0 calc(100% - 9px));
}
.content .inbox-card:hover { background: var(--primary-border-soft); }

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 10px 16px;
  border-bottom: 1px solid var(--panel-border);
}
.panel-head .panel-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--primary);
  white-space: nowrap;
}
.panel-head .panel-title::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  background: var(--primary);
  flex: 0 0 6px;
}
.panel-head .panel-side {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  font-size: 0.75rem;
  color: var(--text-dim);
}
.panel-body { padding: 14px 16px; }

.info-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%;
  border: 1px solid var(--panel-border); color: var(--text-dim);
  font-size: 0.625rem; font-family: 'IBM Plex Mono', monospace; cursor: help;
  vertical-align: middle;
}
.info-icon:hover { border-color: var(--primary); color: var(--primary); }

/* ------------------------------------------------------------- typography */

.page-title { font-size: 1.625rem; margin-bottom: 2px; text-transform: uppercase; }
/* flex + nowrap (the default) lets a subtitle's children -- and, on some
   engines, its own text run -- claim their full content width and push past
   the content column's right edge instead of breaking to a second line;
   flex-wrap: wrap lets both the row of inline pieces (filing/race detail)
   and plain running text (index pages) wrap within the column. */
.page-subtitle {
  color: var(--text-dim); font-size: 0.8125rem; margin-bottom: 16px;
  display: flex; flex-wrap: wrap; align-items: center; min-width: 0;
}
/* No `gap` here: a flex container's own children include the bare text
   runs between inline elements (the browser treats them as anonymous flex
   items), so `gap` lands *before* a text run too -- including one that
   starts with punctuation, e.g. filing_detail.html's "<span>F24</span>,
   received ..." rendered as "F24 , received" with a stray space nothing in
   the template's own text ever put there.

   Removing `gap` alone isn't enough, though: browsers also trim ordinary
   (collapsible) whitespace that sits at the touching edge of an anonymous
   flex item -- a space that is the very first or last character of a text
   run immediately next to a sibling element's tag disappears, on *both*
   sides ("text <span>" loses its trailing space; "</span> text" loses its
   leading space), even with no `gap` at all. So every template using this
   block writes a `&nbsp;` (not a plain space) for any space that touches
   an element boundary -- a non-breaking space isn't collapsible, so it
   survives -- and keeps plain spaces only for whitespace in the middle of
   a text run, away from any tag. See base.html's status-strip and
   filing_detail.html/race_detail.html's page_subtitle blocks. */

.figure { font-size: 1.75rem; }
.small-figure { font-size: 1.125rem; }
.muted { color: var(--text-dim); }
.null-blank:empty::before { content: ""; }

.section-title {
  font-family: 'IBM Plex Mono', monospace;
  text-transform: uppercase; font-size: 0.6875rem; letter-spacing: .12em; color: var(--primary);
  margin: 4px 0 10px 0; display: flex; align-items: center; gap: 8px;
}
.section-title::before { content: ""; display: inline-block; width: 6px; height: 6px; background: var(--primary); }

/* -------------------------------------------------------------- chips */

.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; align-items: center; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--panel-border); border-radius: 16px;
  padding: 0 12px; height: 30px; font-size: 0.78125rem; color: var(--text);
  background: var(--surface-1);
  cursor: pointer;
}
.chip:hover { border-color: var(--primary); text-decoration: none; }
.chip.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
.chip .n { font-family: 'IBM Plex Mono', monospace; color: var(--text-dim); }
.chip.active .n { color: var(--on-accent); }
.chip-link { font-size: 0.78125rem; color: var(--text-dim); }
.chip-link:hover { color: var(--primary); }

/* -------------------------------------------------------------- tables */

.table-wrap { overflow-x: auto; }
/* border-collapse: separate (not collapse) is load-bearing, not a style
   choice: a collapsed table's row/cell borders paint outside the normal box
   model and escape an ancestor's clip-path -- that's exactly what let the
   last row's border-bottom run straight through a chamfered panel's
   bottom-left cut instead of stopping at it. Every table in the app shares
   this one rule; do not give any table its own collapsed-border override,
   or that container's chamfer will show the same overshoot.
   Guarded by scripts/check_no_collapsed_borders.py. */
table { width: auto; min-width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.8125rem; }
/* line-height: IBM Plex Mono's default ("normal") line metrics are taller
   than IBM Plex Sans's at the same font-size -- left unset, a row with any
   mono cell (amount, date, mono code columns) silently grew past
   --row-height while its sans cells didn't, so every row was a few px
   taller than the header row above it. Pinning it here keeps every row --
   header included -- at the same height, which is what actually made the
   header-to-first-row gap read as tighter than the gap between data rows:
   not a missing margin, a row-height mismatch. */
th, td { text-align: left; padding: 0 12px; height: var(--row-height); line-height: 1.3; white-space: nowrap; }
td.wrap, th.wrap { white-space: normal; }
tbody tr { border-bottom: 1px solid var(--panel-border); }
th {
  color: var(--primary); font-weight: 400; text-transform: uppercase; font-size: 0.65625rem;
  letter-spacing: .08em; font-family: 'IBM Plex Mono', monospace;
  position: sticky; top: 0; background: color-mix(in srgb, var(--surface-2) 92%, transparent);
  backdrop-filter: blur(6px);
  z-index: 2; border-bottom: 1px solid var(--panel-border);
}

/* rows that lead somewhere: whole row clickable, pointer, hover tint + left edge.
   Rows that lead nowhere (no .row-link) get no hover tint at all. */
tr.row-link { cursor: pointer; border-left: 2px solid transparent; }
tr.row-link:hover { background: var(--primary-hover-tint); border-left: 2px solid var(--primary); }
tr.row-link td:first-child { padding-left: 10px; }

/* Sortable column headers: th[data-sort] gets a button (see app.js), the
   caret only exists for the currently-sorted column, driven off aria-sort
   so it never drifts out of sync with the actual state. */
th[data-sort] { padding: 0; }
.th-sort-btn {
  all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 4px;
  width: 100%; height: 100%; padding: 0 12px; cursor: pointer;
  font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit;
}
.th-sort-btn:hover { color: var(--text); }
.th-sort-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.th-sort-btn::after { content: ""; width: 8px; flex: none; }
th[aria-sort="ascending"] .th-sort-btn::after { content: "\25B2"; font-size: 0.6em; }
th[aria-sort="descending"] .th-sort-btn::after { content: "\25BC"; font-size: 0.6em; }
/* Right-aligned (num) columns: the caret has to sit on the *left* of the
   label, not trailing after it -- a trailing caret's reserved width (even
   at rest, with no glyph in it, so toggling the sort doesn't shift layout)
   pushes the label itself that far left of the column's right edge, so the
   header text stops lining up with the amounts below it. Swapping the
   reserved space to ::before keeps the label flush right, same as every
   data cell in the column. */
th.num .th-sort-btn { justify-content: flex-end; }
th.num .th-sort-btn::after { content: none; width: 0; }
th.num .th-sort-btn::before { content: ""; width: 8px; flex: none; order: -1; }
th.num[aria-sort="ascending"] .th-sort-btn::before { content: "\25B2"; font-size: 0.6em; }
th.num[aria-sort="descending"] .th-sort-btn::before { content: "\25BC"; font-size: 0.6em; }

/* Item 2: server-side sortable columns on the paginated IE/Money out/Money
   in tables (a click re-queries the server, unlike th[data-sort] above,
   which reorders only the rows already in the DOM -- see index_generic.html
   for why pagination rules that out). */
th.sort-col { cursor: pointer; user-select: none; }
th.sort-col:hover { color: var(--text); }
th.sort-col::after { content: ""; display: inline-block; width: 8px; margin-left: 4px; font-size: 0.6em; }
th.sort-col[aria-sort="ascending"]::after { content: "\25B2"; }
th.sort-col[aria-sort="descending"]::after { content: "\25BC"; }
th.sort-col.sort-active { color: var(--primary); }

.pagination-bar {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: 10px; padding: 12px 14px; border-top: 1px solid var(--panel-border);
}
.pagination-summary { font-size: 0.75rem; }
.pagination-controls { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.pagination-pages { display: flex; align-items: center; gap: 4px; }
.pagination-page.active { color: var(--bg); background: var(--primary); border-color: var(--primary); }
.pagination-ellipsis { padding: 0 4px; }

.links-cell { white-space: nowrap; display: flex; align-items: center; gap: 4px; }
.links-cell .btn-link { display: inline-flex; }

/* --------------------------------------------------------------- tags/pills */

/* Rule tags (SHARED PAYEE, TIMING, ...) are the one place red appears as an
   outline: attention, not decoration -- every rule firing is something that
   wants a human to look at it. */
.tag {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 0.65625rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--alert); border: 1px solid var(--alert); border-radius: 3px; padding: 3px 7px;
  font-family: 'IBM Plex Mono', monospace; background: var(--alert-wash);
  cursor: help;
}

.pill {
  display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 0.6875rem;
  border: 1px solid var(--panel-border); color: var(--text-dim);
}
.pill.memo { color: var(--primary); border-color: var(--primary-border-soft); }
.pill.paired { color: var(--primary); border-color: var(--primary-border-soft); cursor: help; }

/* Money in TYPE column: cash (teal) vs in-kind (yellow), field-derived --
   see app.entities.is_inkind_contribution. Same visual family as .tag
   (mono, uppercase, bordered) without .tag's alert-red default coloring. */
.type-tag {
  display: inline-flex; align-items: center;
  font-size: 0.65625rem; text-transform: uppercase; letter-spacing: .06em;
  border-radius: 3px; padding: 2px 6px;
  font-family: 'IBM Plex Mono', monospace;
}
.type-tag.cash { color: var(--primary); border: 1px solid var(--primary-border-soft); background: transparent; }
.type-tag.inkind { color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); background: transparent; }

/* -------------------------------------------------------------- controls */

button, input, select, textarea {
  font-family: 'IBM Plex Sans', sans-serif;
  background: var(--surface-1); color: var(--text); border: 1px solid var(--panel-border);
  border-radius: 4px; padding: 0 10px; height: 34px; font-size: 0.8125rem;
}
textarea { height: auto; padding: 8px 10px; min-height: 60px; width: 100%; }
select {
  height: 34px;
  /* Replace the native arrow (its own baked-in padding made the box look
     lopsided next to plain inputs/buttons) with one drawn at a fixed inset,
     so the padding around it is ours to control. */
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: 30px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23929e9f' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 10px 6px;
}

button { cursor: pointer; color: var(--text); }
button:hover { background: var(--surface-2); }
button.primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent); font-weight: 600; }
button.primary:hover { filter: brightness(1.08); box-shadow: 0 0 16px -2px var(--accent-glow); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
button:disabled:hover { background: var(--surface-1); }

button.btn-sm, .row-actions button, .links-cell button { height: 26px; padding: 0 8px; font-size: 0.71875rem; }

.row-actions { display: flex; gap: 6px; justify-content: flex-end; }

/* Stopped/over-budget banner: the one full-width alert. A tinted dark wash
   plus a solid red rule, not a solid red fill -- white text on solid
   var(--alert) measures 3.81:1 and fails AA, so red carries the border and
   the text, not the background. */
.banner {
  background: var(--alert-wash); color: var(--text); font-weight: 600;
  padding: 10px 16px; border-bottom: 2px solid var(--alert);
  position: sticky; top: 0; z-index: 20;
}
.banner strong, .banner b { color: var(--alert); }

form.inline { display: inline; }
.controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }

.name-uc { text-transform: uppercase; }
.name-uc.truncate { display: inline-block; max-width: 220px; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; cursor: help; }

.stub-panel { border: 1px dashed var(--panel-border); padding: 12px; color: var(--text-dim); font-size: 0.75rem; border-radius: var(--panel-radius); }

/* ------------------------------------------------------------ memo rows */

.memo-row td { color: var(--text-dim); font-size: 0.75rem; height: 28px; border-bottom: 1px solid var(--panel-border); }
.memo-row .pill { font-style: normal; }

/* ------------------------------------------------------------- pairing */

.pair-row { cursor: pointer; }
.pair-caret {
  display: inline-block; margin-right: 6px; color: var(--text-dim); vertical-align: -1px;
  transition: transform var(--motion-fast) ease;
}
.pair-row.open .pair-caret { transform: rotate(90deg); color: var(--primary); }

/* Expanded transaction lines: one colspan row per underlying transaction,
   the IE table's column set no longer has a slot for everything a line
   needs (full name, stance, purpose, full race name), so this reads as one
   descriptive line rather than parent-column-aligned cells. The "panel" is
   a background tint only -- no border lines, per house style: containers,
   not dividers. */
.pair-detail { display: none; background: var(--surface-1); border-bottom: none; }
.pair-detail.open { display: table-row; }
.pair-detail td { color: var(--text-dim); height: auto; padding-top: 8px; padding-bottom: 8px; white-space: normal; }
.pair-detail-summary td { font-size: 0.6875rem; }
.pair-line { line-height: 1.6; }

.ie-detail-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; line-height: 1.6; }
.ie-detail-sep { padding: 0 2px; }

.filing-link {
  display: inline-flex; align-items: center; gap: 4px;
  font-family: 'IBM Plex Mono', monospace; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--primary);
}
.filing-link svg { flex: none; }
.filing-link:hover { color: var(--accent); text-decoration: none; }

/* --------------------------------------------------------- IE table (one-line rows) */

/* Width hints for the auto-layout table: PAYEE was claiming far more room
   than payee names need (leaving a wide dead gap before AMOUNT) while
   CANDIDATES -- two names plus glyphs -- was the one actually short on
   room. Payee names still wrap (never truncate) if a name is longer than
   this. These are the only two width hints on the table; every other
   column still sizes itself from its own content. */
#tbl-ie th:nth-child(1), #tbl-ie td:nth-child(1) { width: 190px; }
#tbl-ie th:nth-child(4), #tbl-ie td:nth-child(4) { width: 290px; }

.ie-payee { display: flex; align-items: flex-start; gap: 6px; }
.ie-payee .pair-caret { margin-top: 4px; flex: none; }
/* The caret is always rendered (see payee_name_cell in index_generic.html)
   -- a memo-less row gets `visibility: hidden` on it instead of omitting
   the element, so its 8px width + the row's gap are still reserved and
   every name in the column starts at the same x position. */
.pair-caret.caret-reserved { visibility: hidden; }

/* Name + memo-count pill share one line, vertically centered on each
   other via flex (not a baseline/margin nudge) -- if the name wraps to a
   second line, .payee-name-line's own height only grows around its first
   flex row's content in practice (names here rarely wrap; this is the
   simple, common-case-correct answer, not a multi-line-aware layout). */
.payee-name-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.memo-count-pill {
  flex: none;
  font-family: 'IBM Plex Mono', monospace; font-size: 0.6875rem; line-height: 1.4;
  color: var(--text-dim); border: 1px solid var(--primary-border-soft); border-radius: 999px;
  padding: 1px 6px;
}

/* Candidate names read at the row's own font-size/baseline -- only the
   glyph is ever sized down, in its own span, so it never drags the name's
   font-size down with it (that was the actual bug: one span held both the
   arrow and the name text under a single smaller font-size). */
.ie-candidates { display: flex; align-items: baseline; gap: 16px; white-space: nowrap; }
.candidate-chip { display: inline-flex; align-items: baseline; gap: 5px; }
.candidate-arrow { font-size: 0.75em; position: relative; top: -0.05em; }
.arrow-support { color: var(--primary); }
.arrow-oppose { color: var(--alert-text); }

.row-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; color: var(--text-dim);
}
.row-icon-btn:hover { color: var(--accent); }

/* ------------------------------------------------------------ bar charts */

.bar-chart { display: flex; align-items: flex-end; gap: 3px; height: 90px; }
.bar-chart .bar { flex: 1; background: var(--primary); min-height: 2px; border-radius: 1px 1px 0 0; cursor: default; }
.bar-chart .bar.zero { background: var(--panel-border); }
.bar-chart .bar:hover { filter: brightness(1.3); box-shadow: 0 0 10px -2px var(--primary-glow); }

/* --------------------------------------------------------- filter bar / IE brush chart */

.filter-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.filter-bar .search-grow { flex: 1 1 220px; min-width: 160px; }
.filter-bar select { flex: 0 1 auto; }

.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; flex: none;
  border: 1px solid var(--panel-border); border-radius: 4px;
  background: var(--surface-1); color: var(--text-dim); cursor: pointer; text-decoration: none;
}
.icon-btn:hover { border-color: var(--primary); color: var(--primary); background: var(--surface-2); }

.ie-chart-wrap { margin-top: 2px; }
.ie-chart {
  position: relative; height: 45px; width: 100%;
  background: var(--surface-1); border: 1px solid var(--panel-border); border-radius: 4px;
  cursor: crosshair; overflow: hidden; user-select: none;
}
.ie-bars { position: absolute; inset: 0; }
.ie-bar { position: absolute; bottom: 0; background: var(--primary); min-height: 1px; }
.ie-bar.zero { background: var(--panel-border); }
.ie-bar:hover { filter: brightness(1.3); }
.ie-brush {
  position: absolute; top: 0; bottom: 0;
  background: rgba(252, 238, 10, 0.16); border-left: 1px solid var(--accent); border-right: 1px solid var(--accent);
  cursor: grab;
}
.ie-brush:active { cursor: grabbing; }
.ie-handle { position: absolute; top: 0; bottom: 0; width: 10px; cursor: ew-resize; }
.ie-handle-l { left: -5px; }
.ie-handle-r { right: -5px; }
.ie-handle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.ie-chart-labels { display: flex; justify-content: space-between; align-items: center; font-size: 0.6875rem; margin-top: 6px; gap: 8px; }
.ie-range-selected { color: var(--accent); cursor: pointer; background: none; border: none; padding: 2px 4px; border-radius: 3px; }
.ie-range-selected:hover, .ie-range-selected:focus-visible { background: var(--accent-hover-tint); outline: none; }

.ie-popover {
  display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap;
  margin-top: 8px; padding: 10px 12px; background: var(--surface-2);
  border: 1px solid var(--panel-border); border-radius: 6px;
}
.ie-popover[hidden] { display: none; }
.ie-popover label { display: flex; flex-direction: column; gap: 4px; font-size: 0.71875rem; color: var(--text-dim); }
.ie-popover-actions { display: flex; gap: 6px; }

.inline-bar { display: inline-block; width: 90px; height: 8px; background: var(--surface-2); border-radius: 4px; overflow: hidden; vertical-align: middle; margin-right: 8px; }
.inline-bar > span { display: block; height: 100%; background: var(--primary); }

/* Races table: the bar lives alone in its own column, not inline before
   the race name, so it needs neither the trailing margin nor .inline-bar's
   default 90px width label -- same style/scale otherwise (height, surface,
   fill color, rounding all inherited from .inline-bar). 90px per spec. */
.inline-bar.race-bar { width: 90px; margin-right: 0; }
.race-bar-cell { width: 90px; }
/* No min-width: sized to its own content (the longest race name + code in
   this committee's own 2026-cycle data, "Pennsylvania 10th District
   H/PA-10" -- 34 characters -- fits on one line at 1440px/130% without
   forcing extra width elsewhere; a fixed min-width was the actual cause of
   the wide empty gap before the bar column). .table-wrap's own
   overflow-x: auto still takes over below the fit width. */
.race-name-cell { white-space: nowrap; }
/* OTHER WATCHED COMMITTEES / ELECTION DATE: long headers may wrap to two
   lines (an explicit <br> in the template, not word-wrap, so it breaks at
   a sensible point rather than mid-word) -- overrides the generic
   th,td{white-space:nowrap} for just these two <th>s. Letting the header
   wrap, not the data cells, is what lets auto table layout size these
   columns to the (narrower) data instead of the (wider) header text. */
#tbl-races th.wrap-header { white-space: normal; line-height: 1.25; }
/* Slightly tighter than the app's default 12px side padding, evenly on
   every column, so eight columns of numbers still have a clear gap
   between them without the table's total width exceeding its container. */
#tbl-races th, #tbl-races td { padding: 0 8px; }

/* ---------------------------------------------------------------- gauge */

.gauge { display: inline-flex; align-items: center; gap: 6px; }
.gauge-track { display: block; width: 64px; height: 6px; border-radius: 3px; background: var(--surface-2); overflow: hidden; }
.gauge-fill { display: block; height: 100%; background: var(--primary); }
.gauge-fill.warn { background: var(--accent); }
.gauge-fill.danger { background: var(--alert); }

/* --------------------------------------------------------------- status strip */

/* Two rows -- captions, then values -- on one grid, so the spacing between
   them is set once (row-gap) rather than per item, and every caption sits
   on one baseline with every value on another. The ring and the user badge
   span both rows and centre across them. The NOTICE column takes whatever
   width is left; it is the only cell allowed to wrap. */
.status-strip {
  display: grid;
  grid-template-columns: auto auto auto minmax(0, 1fr) auto;
  /* Row 2 is minmax, not a fixed 20px: below the fit width the NOTICE
     sentence folds to a second line and the row has to grow with it. */
  grid-template-rows: 14px minmax(20px, auto);
  grid-template-areas:
    "ring poll-cap ledger-cap notice-cap user"
    "ring poll-val ledger-val notice-val user";
  row-gap: 4px;
  column-gap: 28px;
  align-items: baseline;
  padding: 10px 14px;
  margin-bottom: 16px;
  color: var(--text-dim);
}

.sb-cap {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 10px; line-height: 14px; letter-spacing: 1.5px;
  text-transform: uppercase; color: var(--text-dim);
  white-space: nowrap;          /* captions never wrap, at any width */
}
.sb-val { color: var(--text); }

.sb-cap-poll   { grid-area: poll-cap; }
.sb-cap-ledger { grid-area: ledger-cap; }
.sb-cap-notice { grid-area: notice-cap; }
.sb-countdown  { grid-area: poll-val; }
.sb-ledger     { grid-area: ledger-val; }
.sb-notice     { grid-area: notice-val; }

/* ---- ring: the live indicator, replacing the old pulsing dot.
   30px across, 3px stroke, so r = (30 - 3) / 2 = 13.5 and the
   circumference app.js divides up is 2*pi*13.5 = 84.823px. Rotated -90deg
   so the path starts at twelve o'clock; app.js sets the dash so the arc
   ends there and the gap opens clockwise from it as the interval runs
   down. */
.sb-ring { grid-area: ring; align-self: center; }
.sb-ring svg { display: block; transform: rotate(-90deg); }
.sb-ring-track { fill: none; stroke: var(--ring-track); stroke-width: 3; }
.sb-ring-arc {
  fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: butt;
}
.status-strip.poll-overdue .sb-ring-arc { stroke: var(--alert); }
.status-strip.poll-overdue .sb-cap-poll { color: var(--alert); }

/* ---- next poll countdown.
   Tabular figures plus a floor of four characters ("m:ss", the widest
   string it shows on a five-minute interval): the text is rewritten every
   second and must never change the width of its own column. */
.sb-countdown {
  align-self: baseline;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 15px; line-height: 20px;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  color: var(--text);
  min-width: 4ch;
  white-space: nowrap;
}

/* ---- API ledger: 16 segments, one per sixteenth of the rolling-hour
   limit. Centred rather than baseline-aligned -- it has no text in it, so
   its "baseline" would be the bottom of the last segment. */
/* Pinned to the top of the value row and centred inside a 20px box of its
   own, rather than centred in the row: when NOTICE wraps the row grows,
   and the segments must stay on the values' line instead of drifting down
   with it. */
.sb-ledger { align-self: start; display: flex; align-items: center; height: 20px; gap: 2px; }
.sb-seg { width: 6px; height: 12px; background: var(--surface-2); }
.sb-seg.on { background: var(--primary); }
.sb-ledger.warn.warn .sb-seg.on { background: var(--accent); }
.sb-ledger.danger.danger .sb-seg.on { background: var(--alert); }

/* Narrow column: the five columns no longer fit, so NOTICE takes a row of
   its own underneath rather than shrinking to nothing. Everything else --
   the caption/value pairing, the row gap, the shared baselines -- is
   unchanged; only the areas move. The one margin is the gap between the
   two stacked groups, which a single row-gap cannot express. */
.status-strip-fit { container-type: inline-size; container-name: statusstrip; }
@container statusstrip (max-width: 540px) {
  .status-strip {
    grid-template-columns: auto auto minmax(0, 1fr) auto;
    grid-template-rows: 14px minmax(20px, auto) 14px minmax(20px, auto);
    grid-template-areas:
      "ring poll-cap   ledger-cap   user"
      "ring poll-val   ledger-val   user"
      "notice-cap notice-cap notice-cap notice-cap"
      "notice-val notice-val notice-val notice-val";
  }
  .sb-ring, .status-strip .user-badge { grid-row: 1 / span 2; }
  .sb-cap-notice { margin-top: 6px; }
}

/* ---- notice: the 24-hour-report sentence, in words. */
.sb-notice {
  font-family: 'IBM Plex Sans', sans-serif;
  font-size: 14px; line-height: 20px; color: var(--text);
}

.user-badge {
  font-family: 'IBM Plex Mono', monospace; font-size: 0.6875rem; color: var(--heading);
  border: 1px solid var(--panel-border); border-radius: 4px; padding: 2px 8px;
}
/* Unchanged in look and function -- only its placement is the grid's now. */
.status-strip .user-badge { grid-area: user; align-self: center; }

/* ---------------------------------------------------------- inbox cards */

.inbox-day-header {
  font-family: 'IBM Plex Mono', monospace; font-size: 0.6875rem; text-transform: uppercase;
  letter-spacing: .1em; color: var(--primary); margin: 18px 0 8px 0;
}
.inbox-day-header:first-child { margin-top: 0; }

.inbox-card.rule-hidden { display: none; }

.inbox-card {
  background: var(--panel-bg);
  backdrop-filter: blur(var(--panel-blur));
  -webkit-backdrop-filter: blur(var(--panel-blur));
  border: 1px solid var(--panel-border);
  border-radius: var(--panel-radius);
  box-shadow: inset 0 1px 0 var(--panel-highlight), var(--panel-shadow);
  padding: 12px 14px;
  margin-bottom: 10px;
}
.inbox-card:hover {
  border-color: var(--primary-border-soft);
  box-shadow: inset 0 1px 0 var(--panel-highlight), var(--panel-shadow), 0 0 0 1px var(--primary-border-soft), 0 0 18px -4px var(--primary-glow);
}

/* Upper-right, inset enough to clear the card's chamfered corner cut. */
.star-toggle {
  position: absolute; top: 8px; right: 18px;
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0;
  border: none; background: none;
  cursor: pointer; z-index: 1;
}
.star-toggle:hover { background: none; }
.star-toggle svg { fill: none; stroke: var(--accent); stroke-width: 1.3; stroke-linejoin: round; }
.star-toggle:hover svg { fill: var(--accent); }
.star-toggle.starred svg { fill: var(--accent); }
/* card-top reserves a fixed slot (star footprint + gap) on the right so the
   headline value can never sit under the star toggle; the title wraps into
   that space instead of running underneath it. */
.inbox-card .card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-right: 34px; }
.inbox-card .card-headline { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; font-size: 0.875rem; color: var(--heading); min-width: 0; }
.inbox-card .card-headline .htext { overflow-wrap: anywhere; }
.inbox-card .card-figure { flex: none; font-family: 'IBM Plex Mono', monospace; font-size: 0.6875rem; color: var(--heading); white-space: nowrap; font-variant-numeric: tabular-nums; padding-top: 2px; }
.inbox-card .card-evidence { color: var(--text-dim); font-size: 0.78125rem; margin: 6px 0; line-height: 1.5; }
.inbox-card .card-footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 8px; flex-wrap: wrap; }
.inbox-card .card-times { display: flex; gap: 14px; font-family: 'IBM Plex Mono', monospace; font-size: 0.6875rem; color: var(--text-dim); }
.inbox-card .card-times b { color: var(--text); font-weight: 400; }
.inbox-card .card-actions {
  display: flex; gap: 6px; opacity: 0.6;
}
.inbox-card:hover .card-actions { opacity: 1; }
.inbox-card.dismissed { opacity: 0.55; }

/* -------------------------------------------------------------- glossary */

.glossary-entry { margin-bottom: 16px; }
.glossary-entry:last-child { margin-bottom: 0; }
.glossary-term {
  font-family: 'IBM Plex Mono', monospace; text-transform: uppercase; font-size: 0.6875rem;
  letter-spacing: .1em; color: var(--primary); margin-bottom: 4px;
}
.glossary-def { color: var(--text); font-size: 0.8125rem; line-height: 1.5; }

/* ---------------------------------------------------------------- tooltip */

.tt {
  position: fixed;
  z-index: 100;
  background: var(--surface-2);
  border: 1px solid var(--panel-border);
  box-shadow: inset 0 1px 0 var(--panel-highlight), 0 12px 32px rgba(0,0,0,0.55);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 0.75rem;
  color: var(--text);
  max-width: 340px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 120ms ease;
}
.tt.show { opacity: 1; }
.tt .tt-title {
  font-family: 'IBM Plex Mono', monospace; font-size: 0.625rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--primary); margin-bottom: 4px;
}

/* --------------------------------------------------------------- misc */

.dash { color: var(--text-dim); }
.cycle-toggle { display: inline-flex; border: 1px solid var(--panel-border); border-radius: 16px; overflow: hidden; }
.cycle-toggle a { padding: 0 12px; height: 30px; display: flex; align-items: center; font-size: 0.78125rem; color: var(--text-dim); }
.cycle-toggle a.active { background: var(--accent); color: var(--on-accent); font-weight: 600; }
.cycle-toggle a:hover:not(.active) { color: var(--primary); }

.rule-card {
  border: 1px solid var(--panel-border); border-radius: 6px; padding: 10px 12px; margin-bottom: 8px;
  background: var(--surface-1);
}
.rule-card .rc-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.rule-card .rc-key { color: var(--text-dim); font-family: 'IBM Plex Mono', monospace; font-size: 0.6875rem; }
.rule-card .rc-desc { color: var(--text-dim); font-size: 0.78125rem; margin: 6px 0; }
.rule-card .rc-thresholds { display: flex; gap: 14px; flex-wrap: wrap; font-size: 0.75rem; }
.rule-card .rc-thresholds .k { color: var(--primary); }
.rule-card .rc-thresholds .v { color: var(--text); font-family: 'IBM Plex Mono', monospace; }

.field-row { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid var(--panel-border); font-size: 0.8125rem; }
.field-row:last-child { border-bottom: none; }
.field-row .k { color: var(--text-dim); }
.field-row .v { color: var(--text); font-family: 'IBM Plex Mono', monospace; }

.empty-panel { text-align: center; padding: 32px 16px; color: var(--text-dim); }

/* -------------------------------------------------------- sidebar activity */

/* No side padding on the wrap: the box's width should match the navlinks
   above it, which run edge-to-edge across .nav. The 18px left inset on its
   own head/body (2px more than the navlinks' 16px) accounts for the
   navlinks' own 2px transparent border-left, so the text lines up. */
/* The two sidebar boxes: ghost panels with no padding of their own, so
   their heads, dividers, bodies and rows all sit on the sidebar's content
   edges. One group gap above each. */
.sidebar-committees-wrap, .sidebar-activity-wrap {
  padding: 0;
  margin-top: var(--sb-gap-group);
}
.sidebar-committees, .sidebar-activity { padding: 0; display: flex; flex-direction: column; }
.sidebar-committees .panel-head, .sidebar-activity .panel-head {
  padding: 0 0 var(--sb-gap-head-divider) 0;
  border-bottom: 1px solid var(--panel-border);
  flex: 0 0 auto;
  /* flex-end, not centre: the value on the right (1, 14d) is set a size
     larger than the header label, so centring left the label floating 2px
     above the row's bottom and the gap down to the divider measured 10px
     instead of the 8px the padding asks for. Bottom-aligned, both sit on
     the content box's bottom edge and the 8px is true for both. */
  align-items: flex-end;
}
.sidebar-committees .panel-body, .sidebar-activity .panel-body {
  padding: var(--sb-gap-divider-item) 0 0 0;
  display: flex; flex-direction: column; min-height: 0;
}
.sidebar-committees .panel-body { gap: var(--sb-gap-list-item); }
/* the heat strip and the LATEST block below it */
.sidebar-activity .panel-body { gap: var(--sb-gap-above-latest); }
/* No accent marker on these two headers -- the text starts on the content
   edge where the marker used to be. Panels in the main column keep theirs. */
.sidebar-committees .panel-title::before,
.sidebar-activity .panel-title::before { display: none; }
.sidebar-committees .panel-title, .sidebar-activity .panel-title { gap: 0; }

.committee-row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: 0.75rem; color: var(--text);
}
.committee-name {
  font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Not a .pill: the word alone in the one green the palette has, with no
   border, wash or padding around it. Sized to sit inside the name's own
   line box rather than stand taller than it. */
.committee-live {
  flex: none;
  color: var(--live);
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.625rem;
  line-height: 1.2;
  letter-spacing: .06em;
}
.panel.ghost {
  background: none; border: none; box-shadow: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
/* 14-cell heat strip: one link per day, colour stepped dark-to-bright teal
   by transaction count, peak day in accent yellow. */
.heat-strip { display: flex; gap: 2px; height: 20px; flex: 0 0 auto; }
.heat-cell {
  flex: 1; border-radius: 1px; cursor: pointer;
  background: #0d211f; transition: filter var(--motion-fast) ease;
}
.heat-cell.peak { background: var(--accent); }
.heat-cell:hover { filter: brightness(1.35); }

.sidebar-latest {
  display: flex; flex-direction: column; min-height: 0;
  gap: var(--sb-gap-list-item);
}
.latest-label {
  font-size: 0.625rem; letter-spacing: .1em; color: var(--text-dim);
  flex: 0 0 auto;
}
.latest-row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  color: var(--text); text-decoration: none; border-radius: 3px;
}
.latest-row:hover { color: var(--primary); }
.latest-row .latest-payee {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.75rem;
}
.latest-row .latest-time { color: var(--text-dim); font-size: 0.6875rem; flex: none; }
.latest-row:hover .latest-time { color: var(--primary); }
.latest-empty { font-size: 0.75rem; }

/* ------------------------------------------------------------ rule toggle */

.glossary-label-line { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

.rule-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; flex: 0 0 44px;
  background: none; border: none; padding: 0; margin: 0;
  cursor: pointer;
}
/* The generic button:hover { background: var(--surface-2) } rule filled
   this whole 44px square on hover -- a visible "cube" behind the round
   track. Cancel the fill; the track/thumb color change is the hover cue. */
.rule-toggle:hover { background: none; }
.rule-toggle:hover .rule-toggle-track { border-color: var(--primary); }
.rule-toggle[aria-checked="true"]:hover .rule-toggle-track { border-color: var(--accent); }
.rule-toggle-track {
  display: block; width: 32px; height: 16px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--panel-border);
  position: relative;
}
.rule-toggle-thumb {
  display: block; position: absolute; top: 1px; left: 1px;
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--text-dim);
  transition: transform var(--motion-fast) ease, background-color var(--motion-fast) ease;
}
.rule-toggle[aria-checked="true"] .rule-toggle-track { background: var(--accent-hover-tint); border-color: var(--accent); }
.rule-toggle[aria-checked="true"] .rule-toggle-thumb { transform: translateX(16px); background: var(--accent); }

/* Inbox rail: an off rule's glossary entry dims. Filter chips for an off
   rule dim the same way (never hidden -- items already in the inbox from
   when the rule was on stay reachable). */
.rail .rule-toggle-row.rule-off .glossary-term,
.rail .rule-toggle-row.rule-off .glossary-def { opacity: 0.45; }
.chip.rule-off { opacity: 0.45; }

/* Square-cornered, matching the Sort <select> next to it -- not the pill
   shape shared by the rule chips. */
.chip.starred-filter { border-radius: 4px; height: 34px; }

/* ------------------------------------------------------- AI notice modal */

/* Shown by an attribute the head script sets before the first paint, not
   by JS after load: the dashboard must never be visible, or clickable,
   underneath it for a frame. Hidden by default so a page whose script did
   not run shows nothing rather than an un-dismissable panel. */
.ai-notice-veil { display: none; }
html[data-ai-notice] .ai-notice-veil {
  display: flex;
  align-items: center;
  justify-content: center;
  position: fixed;
  inset: 0;
  /* Above everything this app stacks: the sticky tab bar (20), the user
     picker and the scanline overlay (50). */
  z-index: 200;
  background: var(--veil);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}
/* Nothing behind it scrolls while it is open. The veil itself covers the
   page, so it also takes every click. */
html[data-ai-notice], html[data-ai-notice] body { overflow: hidden; }

/* Same construction as .content .inbox-card: the element's own background
   is the border colour, clipped to the full notch, and a ::before inset
   1px and clipped to a 1px smaller notch paints the glass surface on top,
   leaving a continuous 1px ring -- chamfered corners included. 16px notch
   here rather than the cards' 10px. */
.ai-notice {
  position: relative;
  z-index: 0;
  width: 520px;
  max-width: calc(100vw - 32px);
  padding: 28px;
  display: flex;
  align-items: flex-start;
  gap: 24px;
  background: var(--border);
  clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px));
}
.ai-notice::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  pointer-events: none;
  background: var(--panel-bg);
  backdrop-filter: blur(var(--panel-blur));
  -webkit-backdrop-filter: blur(var(--panel-blur));
  clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 15px, 100% 100%, 15px 100%, 0 calc(100% - 15px));
}

/* The cat sets the panel's height: 28px of padding above and below it, and
   everything in the text column fits inside its 128px. */
.ai-notice-cat {
  flex: none;
  display: block;
  width: 128px; height: 128px;
  image-rendering: pixelated;
  border: 1px solid var(--alert);
}

.ai-notice-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
/* .tag supplies the red, border, fill, mono face, size and letter-spacing;
   only its help cursor is wrong here, since this one explains nothing on
   hover. */
.ai-notice-tag { cursor: default; }
.ai-notice-lines {
  margin: 16px 0 0 0;
  font-size: 20px;
  line-height: 1.35;
  color: var(--heading);
  white-space: nowrap;
}

/* The word alone, in the corner. Positioned clear of the 16px chamfer
   rather than tucked into it. */
.ai-notice-close {
  position: absolute;
  top: 10px; right: 30px;
  padding: 0;
  border: none;
  background: none;
  border-radius: 0;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  color: var(--text-dim);
  cursor: pointer;
}
/* Hover brightens it; focus only rings it. The notice opens with focus
   already on this button, so tying the colour to focus would mean it was
   never seen in its resting state. */
.ai-notice-close:hover { background: none; color: var(--text); }
.ai-notice-close:focus-visible {
  background: none;
  outline: 1px solid var(--primary);
  outline-offset: 2px;
}
/* The notice opens with focus already on this button, and a ring drawn
   around it then would be the box the design does not want -- so the ring
   is suppressed for that one programmatic focus and restored the moment a
   key is pressed, which is when a keyboard user needs it. */
.ai-notice-close[data-quiet-focus]:focus-visible { outline: none; }
