/* Deliberate duplicate of app/static/style.css -- kept in sync manually so
   the Archive's permanent pages share the resolver's visual language
   without coupling the two services' deploys. If you change shared classes
   here (.segment-timestamp, .transcript-list, .agenda-section, etc.),
   change both files. */

:root {
  color-scheme: light;
  --fg: #3f3f3f;
  --bg: #ffffff;
  --page-bg: #f8f9fa;
  --muted: #666;
  --border: #ddd;
  --primary: #2c3e50;   /* navy: headings, primary accents */
  --accent: #3498db;    /* blue: links, buttons, focus */
  --secondary: #e74c3c; /* red: errors/emphasis, used sparingly */
  --pill-bg: #ffe6a1;   /* cream/amber: status & warning callouts */
  --pill-fg: #a84b00;
  --success-bg: #d7f0e0; /* soft green: positive status badges (e.g. "has a transcript") */
  --success-fg: #1e7d4f;
  --error-bg: #fbe2e0;   /* soft red: paired with --secondary for error messages */
  --font-body: Georgia, 'Times New Roman', serif;
  --font-mono: 'IBM Plex Mono', 'Courier New', monospace;
}

body {
  font-family: var(--font-body);
  color: var(--fg);
  background: var(--page-bg);
  margin: 0;
}

.dymo-label {
  background: #b71c1c;
  color: #fff;
  border-radius: 0;
  padding: 0.22em 1.15em 0.18em 1.15em;
  font-family: 'Courier New', monospace;
  font-size: 1.22rem;
  font-weight: bold;
  letter-spacing: 0.11em;
  text-shadow: 0 2px 2px #7b1010, 0 -1px 1px #7b1010;
  display: inline-block;
  line-height: 1.2;
  border: 2px solid #a84b00;
}
/* Narrow enough that the full-size wordmark plus the navbar-toggler
   hamburger no longer fit on one line -- confirmed live at 375px width,
   where the label alone (312px) left the toggler (56px) no room and
   wrapped to a second row. Smaller font/padding/letter-spacing only
   below this width; unchanged everywhere wider, including tablet. */
@media (max-width: 576px) {
  .navbar-brand .dymo-label {
    padding: 0.2em 0.6em 0.16em 0.6em;
    font-size: 0.92rem;
    letter-spacing: 0.05em;
  }
}

/* Manila-folder-tab style divider between nav links -- reuses the same
   cream/amber pair as the .warnings pill so it reads as part of the same
   "manila folder" visual language, not a new color introduced just here. */
.nav-divider {
  width: 5px;
  align-self: stretch;
  margin: 0.35rem 1rem;
  background: var(--pill-bg);
  border: 1px solid var(--pill-fg);
  border-radius: 2px;
  opacity: 0.85;
}

/* Real gap fixed 2026-08-11: base.html now picks the nav's initial state
   from active_account, so a signed-in visitor's "Sign in" flash is gone
   -- but #clerk-user-button starts as a genuinely empty <span> until
   ClerkJS finishes loading and mounts the real avatar into it, so
   everything to its right (nothing today, but any future nav item) would
   still visibly shift once the avatar pops in. :not([hidden]) only
   matches once base.html has already un-hidden this span server-side
   (a signed-in visitor) -- never overrides the real `hidden` attribute
   for a signed-out one, which stays governed by the UA stylesheet as
   before. A same-size neutral circle placeholder (roughly Clerk's own
   default avatar dimensions) reserves the space immediately; Clerk's
   real avatar renders inside this same span once mounted and simply
   covers it, no JS-side cleanup needed. */
#clerk-user-button:not([hidden]) {
  display: inline-block;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
  vertical-align: middle;
}

/* --- Cassette buttons --- */
/* Used on just two buttons (homepage submit, meeting page's "Copy link to
   current time") -- the rewind-to-a-moment metaphor fits those, not every
   button on the page. */
.cassette-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  background: #fff;
  color: #222426;
  border: 2px solid #222;
  border-radius: 7px;
  font-family: 'Courier New', monospace;
  font-size: 1.13rem;
  font-weight: bold;
  padding: 0.54rem 1.3rem;
  cursor: pointer;
  transition: background 0.16s, color 0.16s, border 0.16s;
}
.cassette-btn:hover, .cassette-btn:focus {
  background: #f3f3f3;
  color: #111;
  border-color: #111;
}
/* Disabled state -- currently only "Save this search" while the search
   box/filters have unsaved edits (meeting_list.js's isStale()). Muted
   rather than just a lower opacity, so it doesn't read as "loading". */
.cassette-btn:disabled {
  background: #eee;
  color: #999;
  border-color: #ccc;
  cursor: not-allowed;
}
.cassette-btn:disabled:hover, .cassette-btn:disabled:focus {
  background: #eee;
  color: #999;
  border-color: #ccc;
}
.cassette-label {
  font-family: inherit;
  font-size: 1.08rem;
  letter-spacing: 1.5px;
}
.cassette-reel {
  display: flex;
  align-items: center;
}
.cassette-reel circle { transition: filter 0.2s; }
@keyframes reel-spin { 100% { transform: rotate(360deg); } }
.cassette-btn:hover .cassette-reel,
.cassette-btn:active .cassette-reel {
  animation: reel-spin 0.8s linear infinite;
  transform-origin: 50% 50%;
}
/* Ambient loading state (the "please wait" fetch message) -- slower
   than the hover flourish above since this can run for up to ~20s and
   a quick 0.8s spin reads frantic sustained that long. */
.cassette-reel.spinning {
  animation: reel-spin 1.6s linear infinite;
  transform-origin: 50% 50%;
}

/* "Pop up and glow" attention cue -- user request 2026-08-11, meeting_page.js's
   wireSourceDisclaimerPointer(). Same "depressed vs. popped-up" tape-deck
   button metaphor floated for the search/save-search buttons in
   BACKLOG.md (not built there yet), first real use of it here: a real
   button momentarily lifts and glows to draw the eye across the column,
   then settles back to normal, rather than a static color change that's
   easy to miss on a button already in the viewport. */
@keyframes cassette-btn-pop {
  0% { transform: translateY(0); box-shadow: none; border-color: #222; }
  35% { transform: translateY(-6px); box-shadow: 0 8px 16px rgba(52, 152, 219, 0.45); border-color: var(--accent); }
  100% { transform: translateY(0); box-shadow: none; border-color: #222; }
}
.cassette-btn.pointed-to {
  animation: cassette-btn-pop 900ms ease;
}

.status-loading {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
}

/* Input group pairing (URL textbox + cassette submit button fused into
   one control), matching round-1's real input-group/cassette-btn sizing. */
.input-group > .form-control,
.input-group > .cassette-btn {
  height: 48px;
  min-height: 48px;
  font-size: 1.05rem;
}
.input-group > .form-control {
  border-radius: 7px 0 0 7px;
}
.input-group > .cassette-btn {
  border-radius: 0 7px 7px 0;
}

.center-page {
  max-width: 640px;
  margin: 10vh auto;
  padding: 0 1.5rem;
  text-align: center;
}

.subtitle { color: var(--muted); }

.error { color: var(--secondary); }
.status { color: var(--muted); margin: 1rem 0; }

.meeting-page {
  max-width: 860px;
  margin: 2rem auto;
  padding: 0 1.5rem;
}

/* Desktop-only: video pinned in a narrow sticky left column, transcript
   scrolling in a wider right column -- see app/static/style.css's
   identical block (kept in sync manually) for the full reasoning: the
   video can stay genuinely narrow/small since most viewers here just
   need audio plus a visual confirmation of who's speaking, not a large
   frame for reading slides.

   #videoColumn wraps #videoSection together with the report-problem and
   transcribe-request toggles/forms into a single sticky unit -- these
   used to be separate grid items sharing #videoSection's row, which let
   one sticky element overlap the other as the page scrolled (real bug
   from live review). One sticky box, sized to its own real content,
   removes that whole class of problem. */
@media (min-width: 900px) {
  .meeting-page {
    display: grid;
    grid-template-columns: minmax(220px, 300px) 1fr;
    column-gap: 1.75rem;
    align-items: start;
  }
  .meta { grid-column: 1 / -1; }
  #videoColumn {
    grid-column: 1;
    position: sticky;
    top: 1rem;
  }
  .toolbar { position: static; }
  #transcriptColumn {
    grid-column: 2;
    /* Grid items default to min-width: auto, which sizes a track to fit
       its content's minimum intrinsic width -- for an unbreakable string
       (a long agenda-PDF URL with no spaces/hyphens to wrap on), that's
       the string's full rendered width, pushing this track past its fair
       1fr share and .meeting-page past its own max-width. Same fix as
       .calendar-candidate-main/.saved-item-main below. */
    min-width: 0;
  }
}

.meta h1 { margin-bottom: 0.2rem; color: var(--primary); }
/* margin-bottom wasn't overridden here before -- the browser's default
   paragraph spacing (~1em) was the real cause of "too much space"
   between the jurisdiction/date line and "View original source" below
   it, not .source-link's own (already tight) margin. */
.meta p { color: var(--muted); margin: 0 0 0.25rem; }
.source-link { margin: 0.15rem 0; }
.source-link a {
  color: var(--muted);
  font-size: 0.85rem;
  text-decoration: none;
}
.source-link a:hover { color: var(--accent); text-decoration: underline; }

.link-button {
  background: none;
  border: none;
  padding: 0;
  color: var(--muted);
  font-size: 0.85rem;
  text-decoration: underline;
  cursor: pointer;
}
.link-button:hover { color: var(--accent); }
.report-problem-toggle { margin: 0.4rem 0 0; }
.report-problem-form {
  margin-top: 0.75rem;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  max-width: 420px;
}
.report-problem-form label {
  display: block;
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 0.75rem;
}
.report-problem-form select,
.report-problem-form textarea {
  display: block;
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-family: inherit;
  font-size: 0.9rem;
}
.report-problem-actions { display: flex; align-items: center; gap: 1rem; }
/* Status message pill -- matches .warnings' shape/weight (below) instead
   of a smaller, plain-colored-text treatment, so a "your report was
   sent" confirmation reads as part of the same status-message visual
   language as everything else on the page, not a separate smaller
   system. Empty by default (no text yet), so no visible pill until a
   real status message is set. */
.report-problem-status,
.transcribe-status,
.refresh-page-status {
  display: inline-block;
  border-radius: 7px;
  padding: 0.35rem 0.85rem;
  font-weight: bold;
  margin: 0.6rem 0 0;
}
.report-problem-status:empty,
.transcribe-status:empty,
.refresh-page-status:empty { display: none; }
.report-problem-status.success,
.transcribe-status.success,
.refresh-page-status.success { background: var(--success-bg); color: var(--success-fg); }
.report-problem-status.error,
.transcribe-status.error,
.refresh-page-status.error { background: var(--error-bg); color: var(--secondary); }

.refresh-page-toggle { margin: 0.4rem 0 0; }

.transcribe-toggle { margin: 0.4rem 0 0; }
.nyc-crosslink { margin: 0.75rem 0 0; font-size: 0.9rem; }
.transcribe-form {
  margin-top: 0.75rem;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  max-width: 420px;
}
.transcribe-form label {
  display: block;
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 0.75rem;
}
.transcribe-form input[type="email"] {
  display: block;
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-family: inherit;
  font-size: 0.9rem;
}

/* Cream/amber "choose wisely" pill treatment for status & warning
   messages -- the manila-folder half of the red-tape/manila-folder vibe. */
.warnings {
  display: inline-block;
  background: var(--pill-bg);
  color: var(--pill-fg);
  border-radius: 7px;
  padding: 0.35rem 0.85rem;
  font-weight: bold;
  margin-bottom: 0.75rem;
}
.warnings:empty { display: none; }
.warnings a { color: inherit; text-decoration: underline; }
.warnings a:hover { color: var(--accent); }
/* Real gap fixed 2026-08-11: app/static/style.css (the file this one is
   deliberately kept in sync with, see this file's own header comment)
   already had this rule; it never got copied over here, so every
   .transcribe-inline-trigger this service renders (render_warnings.py's
   warnings-text buttons, and meeting_page.html's own
   #sourceDisclaimerPointer link) rendered as an unstyled default button/
   link instead of the intended plain-link look. */
.transcribe-inline-trigger {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.transcribe-inline-trigger:hover { color: var(--accent); }

/* Plain "we think we found this: <link>" lines (agenda_link, 2026-08-10)
   -- deliberately normal weight/color, not the bold-on-tinted-background
   .warnings treatment, since nothing here is confirmed the way a real
   warning is. Mirrored from app/static/style.css, including the
   long-URL-overflow fix confirmed live there. */
.source-guess { color: var(--fg); margin: 0 0 1rem; overflow-wrap: break-word; }
.source-guess a { color: inherit; text-decoration: underline; overflow-wrap: break-word; }
.source-guess a:hover { color: var(--accent); }

.video-section { margin: 1.5rem 0; }
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
  /* On long transcripts with auto-scroll on, the page keeps scrolling the
     transcript into view, making it hard to scroll back up to turn
     auto-scroll off (real complaint from live review). Sticking the
     toolbar to the top keeps it reachable at all times. */
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--page-bg);
  padding: 0.5rem 0;
}
.seek-form { display: flex; gap: 0.4rem; margin-left: auto; }
.seek-form input {
  width: 6.5rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 0.9rem;
}
/* Auto-scroll toggle -- ported from app/static/style.css's identical
   #toggleAutoScrollBtn/.video-subtoolbar block, the resolver-page-only
   control every permanent /m/{slug} page was missing entirely. */
.video-subtoolbar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
#toggleAutoScrollBtn { width: 100%; }
/* Positioning context for the floating .copy-toast bubble below --
   wraps just the button (not the whole .toolbar, whose own position
   flips between sticky/static across breakpoints) so the toast always
   anchors to the button itself regardless of layout. */
.copy-control {
  position: relative;
  display: inline-block;
}
/* Small fading confirmation that floats above the share button (real
   feedback: below/beside it went unnoticed or crowded the row) --
   replaces the previous behavior of swapping the button's own label
   text to "Copied!", since the label now shows a live timestamp instead.
   An absolutely-positioned overlay, not in-flow, so it never shifts
   surrounding layout when it appears. Shown via a .visible class
   (opacity transition), removed by JS after a few seconds. */
.copy-toast {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: 0.5rem;
  white-space: nowrap;
  background: var(--success-bg);
  color: var(--success-fg);
  border-radius: 6px;
  padding: 0.3rem 0.7rem;
  font-size: 0.8rem;
  font-weight: bold;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
.copy-toast.visible { opacity: 1; }
.video-wrapper {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: 8px;
  overflow: hidden;
}
video { width: 100%; height: 100%; display: block; }

.youtube-player-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
/* YT.Player sets explicit pixel width/height attributes on the iframe it
   creates -- override with !important so it still fills the responsive
   16:9 wrapper instead of the fixed default size. */
.youtube-player-container iframe {
  width: 100% !important;
  height: 100% !important;
}

/* Viebit's embed page is iframed directly (see viebit.py's module
   docstring) -- this element IS the iframe, unlike
   .youtube-player-container above, so the sizing rule applies straight
   to it, no child selector needed. */
.viebit-player-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: none;
}

.big-play-button {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  transition: transform 0.15s ease;
}
.big-play-button:hover { transform: translate(-50%, -50%) scale(1.08); }
.big-play-button svg { display: block; filter: drop-shadow(0 2px 6px rgba(0,0,0,0.4)); }

.transcript-section h2 { margin-bottom: 0.5rem; color: var(--primary); }
.transcript-export { color: var(--muted); font-size: 0.85rem; margin: 0 0 0.75rem; }
.transcript-export a { color: var(--muted); }
.transcript-export a:hover { color: var(--accent); text-decoration: underline; }
.agenda-section { margin-bottom: 1.5rem; }
.agenda-section h2 { margin-bottom: 0.5rem; color: var(--primary); }
/* Shorter than the main transcript list's 60vh -- real feedback: a long
   agenda pushed the "Transcript" heading below the fold. The agenda is
   secondary/reference material here, so it gets its own scrollable box
   rather than claiming the same generous height as the transcript. */
.agenda-section .transcript-list { max-height: 220px; }

/* Fills the space where the transcript would be when there isn't one --
   a live playhead readout so it's clear we're still tracking the exact
   moment, not just a dead end, plus a prominent way to grab a link to
   it (the toolbar's "Copy link to current time" does the same thing,
   but easy to miss when there's nothing else to look at down here). */
.no-transcript-timestamp {
  text-align: center;
  padding: 2.5rem 1rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
}
.no-transcript-hint {
  color: var(--muted);
  max-width: 32rem;
  margin: 0 auto 1.25rem;
}
.no-transcript-time {
  font-family: var(--font-mono);
  font-size: 3.5rem;
  font-weight: bold;
  color: var(--primary);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  margin-bottom: 1.25rem;
}
.transcript-list {
  max-height: 60vh;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1rem;
  background: var(--bg);
  /* Always-visible scrollbar, not just on hover/scroll -- real feedback:
     this box not obviously reading as "a scrollable window" (most
     modern OS/browser combos use invisible-until-interacted overlay
     scrollbars by default). Firefox: */
  scrollbar-width: thin;
  scrollbar-color: var(--border) var(--bg);
}
/* Chrome/Safari/Edge: styling ::-webkit-scrollbar at all switches these
   browsers from overlay to always-reserved-space classic scrollbars. */
.transcript-list::-webkit-scrollbar { width: 10px; }
.transcript-list::-webkit-scrollbar-track { background: var(--bg); }
.transcript-list::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 5px;
}
.transcript-list::-webkit-scrollbar-thumb:hover { background: var(--muted); }
/* Grid, not plain inline flow -- a wrapped line of .segment-text
   previously fell back to the container's far-left edge (under the
   timestamp) instead of aligning under where the text itself started,
   confirmed hard to read on any real multi-line cue. Fixed-width
   timestamp/button columns (auto-sized, never wrap) plus a remaining-
   width text column (wraps within its own column only) keeps every
   wrapped line's left edge aligned under the first line's text. Kept in
   sync with app/static/style.css's identical fix, same "shared markup/
   CSS pattern" note at the top of this file. */
.transcript-segment {
  display: grid;
  /* Fixed (not auto) first column so the timestamp width doesn't vary row
     to row -- each .transcript-segment is its own independent grid
     container, so an auto-sized column only fits *that row's* own
     timestamp text, causing agenda/transcript text to start at a
     different x position per row (most visible on agenda, which spans
     [0:05] to potentially [840:00] all in one viewport). 8.5ch
     comfortably fits the longest real timestamp shape at the 14-hour
     transcription cap, in the monospace font .segment-timestamp already
     uses. */
  grid-template-columns: 8.5ch auto 1fr;
  column-gap: 0.4rem;
  align-items: start;
  margin-bottom: 0.75rem;
}
.transcript-segment.playing { background: rgba(52, 152, 219, 0.15); border-radius: 4px; }
.segment-timestamp {
  color: var(--accent);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  font-family: var(--font-mono);
  white-space: nowrap;
}
.segment-timestamp:hover { text-decoration: underline; }
.segment-text { min-width: 0; }
/* meeting_page.html's unreliable_timestamps case renders .segment-text as
   the sole child (no timestamp/button) when a source reports every agenda
   item at the same instant -- without this, the 3-column grid would
   confine that lone child to the first auto-sized column instead of the
   full row width. */
.transcript-segment > .segment-text:only-child { grid-column: 1 / -1; }

.segment-link-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.7rem;
  height: 1.7rem;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--muted);
  border-radius: 4px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s ease, color 0.1s ease, background 0.1s ease;
}
.segment-link-btn svg { width: 17px; height: 17px; }
.transcript-segment:hover .segment-link-btn,
.segment-link-btn:focus-visible {
  opacity: 1;
}
.segment-link-btn:hover { color: var(--accent); background: rgba(52, 152, 219, 0.12); }
.segment-link-btn.copied { opacity: 1; color: #2f855a; }

/* While the video is at rest (paused), gently suggest that the current
   line is deep-linkable by showing its icon without needing a hover --
   but only the current line, and only at rest, so it doesn't flicker from
   segment to segment during normal playback/auto-scroll. */
body.video-at-rest .transcript-segment.playing .segment-link-btn {
  opacity: 0.6;
}
body.video-at-rest .transcript-segment.playing .segment-link-btn:hover {
  opacity: 1;
}

.transcript-search {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
.transcript-search input {
  flex: 1;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 0.9rem;
}
.transcript-search-count {
  color: var(--muted);
  font-size: 0.85rem;
  min-width: 3.5rem;
  text-align: right;
}

mark.search-match {
  background: #fef08a;
  color: #1a1a1a;
  border-radius: 2px;
  padding: 0 1px;
}
mark.search-match.current { background: #f97316; color: #fff; }

.calendar-candidate {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--border);
}
.calendar-candidate:last-child { border-bottom: none; }
.calendar-candidate a { color: var(--accent); text-decoration: none; font-weight: 500; }
.calendar-candidate a:hover { text-decoration: underline; }
.calendar-candidate-date {
  color: var(--muted);
  display: block;
  margin-top: 0.15rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Real bug fixed 2026-08-11: a long title or jurisdiction on /meetings
   results used to wrap freely, breaking the row's compact one-line-per-
   meeting rhythm. .calendar-candidate-main already has min-width:0 (see
   the comment above .meeting-result-row) so ellipsis truncation actually
   has room to kick in inside the flex row instead of the text just
   overflowing its container. Title truncation is scoped to this specific
   row layout (.calendar-candidate-main), not .calendar-candidate itself,
   since that class is also reused unmodified by the resolver's
   calendar-picker list (renderCalendarPage() in player.js). */
.calendar-candidate-main a {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* /meetings-listing-only layout: title+meta on the left, a fixed badge
   column on the right, so the badge always lands in the same vertical
   line of sight regardless of how long a given row's title/date/
   jurisdiction text runs. A modifier alongside .calendar-candidate
   (not a change to that class itself) -- it's also used, unmodified,
   by the resolver's calendar-picker list (renderCalendarPage() in
   player.js), which doesn't have this two-level title/badge structure
   and would misrender if .calendar-candidate itself became a flex
   container here. */
.meeting-result-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
}
.calendar-candidate-main { min-width: 0; }
/* Matched-context excerpt under a search result (crud.py's list_pages(),
   via utils/search.py's find_snippet()) -- reuses mark.search-match's
   existing highlight color (already used by the in-page transcript
   search) rather than introducing a second one, so "this is a matched
   term" reads the same way everywhere on the site. Only ever present
   when a keyword search is active and matched outside the title/
   jurisdiction (already shown directly above), so it's real added
   context, not a repeat of what's already on the row. */
.search-snippet {
  margin: 0.3rem 0 0;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.4;
}
/* Replaces a raw language code ("· en") on /meetings results -- not
   intuitive at a glance, and language-specific anyway when the real
   question a viewer has is just "does this have a transcript." Quality-
   aware (archive/db/crud.py's list_pages() only sets has_transcript=True
   for a non-garbled default version), so this badge is a real signal,
   not just "a TranscriptVersion row exists."

   Styled as a real pill badge (small icon-like check + small-caps-ish
   label, soft green success color, own background/border) rather than
   colored inline text -- per direct design feedback: the label only
   needs to be read once before a viewer recognizes it purely by shape/
   color afterward, so it can run small; a pill keeps it on one line and
   visually distinct from plain body text; pinned to its own column on
   the right (via .meeting-result-row above) instead of inline after a
   variable-length middot-separated meta string, so it lands in the same
   place on every row.

   A little rubber-stamp flavor -- slight rotation, monospace caps, a
   flat 2px border instead of a soft pill -- fits the site's existing
   "Red Tape Recordings" government-document motifs (the dymo-label
   wordmark, cassette buttons) instead of a generic modern badge.
   Deliberately restrained: no texture/grunge image, no ink-bleed
   filter, just enough tilt to read as "stamped," not silly. */
.transcript-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  flex: none;
  white-space: nowrap;
  background: var(--success-bg);
  color: var(--success-fg);
  border: 2px solid var(--success-fg);
  border-radius: 4px;
  padding: 0.15em 0.6em;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  transform: rotate(-4deg);
}
/* Real bug fixed 2026-08-11: an agenda-only row used to omit
   .transcript-badge entirely, leaving .calendar-candidate-main as the
   row's only flex child -- it then expanded to fill the row, wrapping
   the title wider than a row with a badge reserving that column's
   width. meeting_list.html now always renders the badge markup, just
   invisible (same box, same dimensions) when there's no transcript. */
.transcript-badge-placeholder {
  visibility: hidden;
}

/* "Upcoming" / "Recent" date-status pill (archive/utils/date_status.py):
   inline next to the date on /meetings rows and leading the notice on a
   meeting page. Same mono/uppercase voice as .transcript-badge but flat,
   untilted and neutral-toned -- it's a status note, not the "we have it"
   stamp, and shouldn't compete with the real transcript badge. */
.date-status-pill {
  display: inline-block;
  vertical-align: 0.1em;
  padding: 0.05em 0.45em;
  border: 1px solid var(--muted);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  line-height: 1.5;
  color: var(--muted);
  white-space: nowrap;
}
.date-status-notice {
  margin: 0.4rem 0 0;
  font-size: 0.9rem;
  color: var(--muted);
}
.date-status-notice .date-status-pill {
  margin-right: 0.35em;
}

/* --- About page --- */
.about-page {
  max-width: 640px;
  margin: 3rem auto;
  padding: 0 1.5rem;
  line-height: 1.6;
}
.about-page h1 { color: var(--primary); }
.about-page h2 { color: var(--primary); margin-top: 2rem; }
.about-lede { font-weight: bold; font-size: 1.15rem; }
.about-actions { padding-left: 1.25rem; }
.about-actions li { margin-bottom: 0.5rem; }

/* /coverage's "By platform"/"Custom Platforms" sections group several
   example rows (crud.py's _select_examples()) under one platform-name
   heading -- .meeting-list/.calendar-candidate/.meeting-result-row above
   already do the per-example row styling (same classes /meetings uses),
   this just adds the small group label sitting above them and spacing
   between one platform's group and the next. */
.platform-groups { margin-top: 1rem; }
.platform-group { margin-bottom: 1.75rem; }
.platform-group:last-child { margin-bottom: 0; }
.platform-group-label {
  color: var(--primary);
  font-size: 1rem;
  font-weight: 700;
  margin: 0 0 0.25rem;
}
.platform-group-empty {
  color: var(--muted);
  margin: 0;
}

/* --- Footer / newsletter signup --- */
.site-footer {
  margin-top: 3rem;
  padding: 2rem 1.5rem;
  border-top: 1px solid var(--border);
  background: var(--bg);
}
.footer-subscribe-prompt {
  max-width: 420px;
  margin: 0 auto;
  text-align: center;
  color: var(--muted);
  font-size: 0.9rem;
}
.newsletter-form {
  max-width: 420px;
  margin: 0 auto;
  text-align: center;
}
.newsletter-input-group input {
  height: 48px;
  border: 1px solid var(--border);
  border-radius: 7px 0 0 7px;
  background: var(--bg);
  color: var(--fg);
  font-size: 1.05rem;
}
/* A new button, not cassette-btn -- "sign up" isn't a "rewind to a
   moment" action, so it gets its own but-consistent treatment: same
   bold mono/chunky-border family, solid navy instead of the reel-icon
   gimmick, sized to fuse with the input the same way cassette-btn does
   on the homepage. */
.newsletter-btn {
  height: 48px;
  padding: 0 1.3rem;
  background: var(--primary);
  color: #fff;
  border: 2px solid var(--primary);
  border-radius: 0 7px 7px 0;
  font-family: var(--font-mono);
  font-size: 1.05rem;
  font-weight: bold;
  letter-spacing: 0.5px;
  cursor: pointer;
  transition: background 0.16s, border-color 0.16s;
}
.newsletter-btn:hover, .newsletter-btn:focus {
  background: #1c2a37;
  border-color: #1c2a37;
}
.newsletter-message {
  margin-top: 0.5rem;
  font-size: 0.85rem;
}
.newsletter-message-success { color: #2f855a; }
.newsletter-message-error { color: var(--secondary); }

/* Small brand callback on the dedicated /subscribe page -- reuses the
   dymo-label motif (otherwise only used for the site wordmark) as a
   secondary "section tag" rather than introducing a new visual idea
   just for this one page. */
.dymo-label-small {
  background: #b71c1c;
  color: #fff;
  font-family: 'Courier New', monospace;
  font-size: 0.8rem;
  font-weight: bold;
  letter-spacing: 0.11em;
  padding: 0.2em 0.8em;
  border: 2px solid #a84b00;
  display: inline-block;
  margin-bottom: 1rem;
}

/* --- Archive-only additions (permanent meeting pages) --- */
/* A dropdown, not the original plain link list -- user request 2026-08-11,
   see meeting_page.html's version_picker() macro comment. Inline, since
   it now sits inline with the "Download: Text · SRT" line rather than as
   its own block above the whole transcript section. */
.version-picker {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.version-picker label { color: var(--muted); }
.version-picker select {
  font-family: inherit;
  font-size: 0.85rem;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.15rem 0.4rem;
}

/* AI-transcribed-version disclaimer -- reuses the dymo-label-small motif
   as a real visual flag rather than blending into the plain amber
   .warnings pill treatment every other transcript-quality message uses.
   This one specifically needs to stand out: it's telling a reader the
   text they're about to read might contain fabricated sentences. */
.ai-disclaimer {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  margin-bottom: 0.75rem;
  padding: 0.75rem 1rem;
  background: var(--pill-bg);
  border: 1px solid var(--pill-fg);
  border-radius: 6px;
}
.ai-disclaimer .dymo-label-small {
  flex: none;
  margin-bottom: 0;
}
.ai-disclaimer p {
  margin: 0;
  color: var(--pill-fg);
  font-size: 0.9rem;
}
/* Parallel disclaimer for source-provided (non-AI) transcripts, user
   request 2026-08-11 -- see BACKLOG.md and meeting_page.html. A
   different background/border/text color than .ai-disclaimer's amber
   (blue, matching --accent) so the two read as visually distinct boxes
   at a glance, not just distinguishable by reading the copy -- the
   shared .dymo-label-small pill stays its usual red, but its own text
   ("SOURCE TRANSCRIPT" vs "AI TRANSCRIPT") already differs. */
.source-disclaimer {
  background: #dceefb;
  border-color: var(--accent);
}
.source-disclaimer p {
  color: #1c5d85;
}
.source-disclaimer a {
  color: inherit;
  text-decoration: underline;
}

.meeting-list-page {
  max-width: 720px;
  margin: 2rem auto;
  padding: 0 1.5rem;
}
.meeting-list-page h1 { color: var(--primary); margin-bottom: 0.2rem; }

.meeting-search-form {
  display: flex;
  gap: 0.5rem;
  margin: 1.25rem 0 0.75rem;
}
/* Deliberately reuses .cassette-btn (the same prominent style as the
   Search button above it), not .cassette-btn-outline -- the muted
   "secondary action" style read as disregarded/hard to see against the
   page's grey background. Centered so it doesn't sit left-aligned in
   otherwise-empty horizontal space. */
.save-search-toggle { text-align: center; margin: 1.25rem 0; }
.bookmark-icon { fill: currentColor; }

/* Bookmark toggle icon next to the meeting title (meeting_page.html) --
   outline by default, fills in once saved, mirroring #saveMeetingBtn's
   own text below it (kept in sync by meeting_page.js). */
.bookmark-icon-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0 0 0.1rem 0.4rem;
  vertical-align: middle;
  color: var(--muted);
  line-height: 1;
}
.bookmark-icon-btn:hover { color: var(--primary); }
.bookmark-icon-btn .bookmark-icon { fill: none; }
.bookmark-icon-btn[data-saved="true"] { color: var(--primary); }
.bookmark-icon-btn[data-saved="true"] .bookmark-icon { fill: currentColor; }

.meeting-search-form input {
  flex: 1;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 0.95rem;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Search tips: was a plain always-visible sentence, replaced with a "?"
   icon + popover (2026-08-11) once the tip grew a real operator list --
   too long to sit inline under the search box on every page load. Shown
   on hover/keyboard-focus via CSS alone; also click-toggled via
   meeting_list.js (`.is-open`) since touch devices have no hover state. */
.search-hint { display: flex; align-items: center; gap: 0.4rem; position: relative; }
.search-help-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.15rem;
  height: 1.15rem;
  border-radius: 50%;
  border: 1px solid var(--muted);
  background: var(--bg);
  color: var(--muted);
  font-size: 0.75rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.search-help-icon:hover, .search-help-icon:focus { color: var(--accent); border-color: var(--accent); }
.search-help-panel {
  position: absolute;
  z-index: 5;
  top: 1.6rem;
  left: 0;
  width: max-content;
  max-width: 22rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
  padding: 0.75rem 0.9rem;
  font-size: 0.85rem;
  color: var(--fg);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.search-hint:hover .search-help-panel,
.search-hint:focus-within .search-help-panel,
.search-help-panel.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}
.search-help-panel ul { margin: 0.4rem 0 0; padding-left: 1.1rem; }
.search-help-panel li { margin-bottom: 0.35rem; }
.search-help-panel li:last-child { margin-bottom: 0; }
.search-help-panel code {
  font-family: var(--font-mono);
  background: var(--page-bg);
  border-radius: 3px;
  padding: 0.05rem 0.3rem;
  font-size: 0.82em;
}

.meeting-filters { margin-bottom: 1.5rem; }
.meeting-filters summary {
  cursor: pointer;
  color: var(--accent);
  font-size: 0.9rem;
  margin-bottom: 0.5rem;
}
.meeting-filters-form {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding-top: 0.5rem;
}
/* Each logical group (text/date fields, checkboxes, actions) is its own
   row -- previously one flat flex-wrap container let a narrow checkbox
   land on whatever row had leftover space next to unrelated fields,
   which read as messy/unintentional grouping rather than a deliberate
   layout. */
.filters-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: end;
}
.meeting-filters-form label {
  display: flex;
  flex-direction: column;
  font-size: 0.8rem;
  color: var(--muted);
  gap: 0.25rem;
}
.meeting-filters-form input {
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 0.9rem;
}
.meeting-filters-form label.checkbox-filter {
  flex-direction: row;
  align-items: center;
  gap: 0.4rem;
}
.meeting-filters-form input[type="checkbox"] {
  width: auto;
  padding: 0;
  border: none;
  accent-color: var(--accent);
}
.filters-row-actions { align-items: center; }
/* Lighter-weight sibling to .cassette-btn for a secondary action ("Clear
   all filters") sitting right next to the primary "Apply filters" button
   -- same shape/font so it still reads as a real button, not a link easy
   to miss, but visually quieter so it doesn't compete with the primary
   action. */
.cassette-btn-outline {
  display: inline-flex;
  align-items: center;
  background: transparent;
  color: var(--muted);
  border: 2px solid var(--border);
  border-radius: 7px;
  font-family: 'Courier New', monospace;
  font-size: 1rem;
  font-weight: bold;
  padding: 0.5rem 1.1rem;
  text-decoration: none;
  transition: background 0.16s, color 0.16s, border 0.16s;
}
.cassette-btn-outline:hover, .cassette-btn-outline:focus {
  color: var(--fg);
  border-color: var(--muted);
  background: var(--page-bg);
}

.meeting-list { margin-top: 1rem; }

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  margin: 1.5rem 0;
}
.pagination a { color: var(--accent); text-decoration: none; }
.pagination a:hover { text-decoration: underline; }
.pagination-status { color: var(--muted); font-size: 0.9rem; }

/* --- Footer -- kept in sync with app/static/style.css, see file header --- */
.site-footer {
  margin-top: 3rem;
  padding: 2rem 1.5rem;
  border-top: 1px solid var(--border);
  background: var(--bg);
}
.footer-subscribe-prompt {
  max-width: 420px;
  margin: 0 auto;
  text-align: center;
  color: var(--muted);
  font-size: 0.9rem;
}
.footer-links {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  margin: 0 auto 1.25rem;
  font-size: 0.9rem;
}
.footer-links a { color: var(--muted); }
.footer-links a:hover { color: var(--fg); }

/* --- My saved items (/account/saved) --- */
/* Every section heading gets the same divider + breathing room above it
   (separates it from whatever came before -- the page title, or the
   previous section's list) and a small gap below (keeps it visually
   bonded to its own content) -- same asymmetric spacing convention
   .about-page h2 already uses elsewhere on this site. */
.saved-items-page h2 {
  margin: 2rem 0 0.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
  color: var(--primary);
}
.saved-item-list { margin-top: 0.5rem; }
.saved-item-row {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--border);
}
.saved-item-row:last-child { border-bottom: none; }
.saved-item-main { min-width: 0; }
.saved-item-main a { color: var(--accent); text-decoration: none; font-weight: 500; }
.saved-item-main a:hover { text-decoration: underline; }
/* Sits immediately left of the title/meta, same fixed gap on every row
   regardless of how long the title is -- not pushed to the far edge of
   the page via space-between, which is what made the old layout feel
   disconnected on short titles. */
.unsave-btn { flex-shrink: 0; }

/* --- /coverage's "Every place we've covered" table --- */
/* Archive-only page (app/templates has no coverage.html, resolver just
   proxies the rendered HTML) -- unlike most of this file, no need to
   keep this block in sync with app/static/style.css. */
.coverage-table .row-number-col {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bg);
  font-weight: 400;
  color: var(--muted);
  text-align: right;
  padding-right: 0.75rem;
  /* Only ever holds a small integer -- no reason to compete for space
     with the title/transcript columns below, which actually need room. */
  width: 2rem;
  max-width: 2rem;
  font-size: 0.8rem;
}
/* "Example meeting" (title) and "Transcript" columns used to have no
   width constraint at all, so a long meeting title could stretch the
   whole table wide enough to force horizontal scroll and push the
   Transcript column off-screen on the live /coverage page. Capping
   both with max-width + ellipsis keeps every row a predictable width
   regardless of title length; the full title is still available via
   the link's title="" tooltip and the /m/{slug} page itself. */
.coverage-table .coverage-title-col {
  max-width: 14rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.coverage-table .coverage-transcript-col {
  max-width: 9rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Bootstrap 5.3's own table-striped overlay (rgba(0,0,0,.05)) applied
   directly to the frozen column too, since its own explicit background
   above would otherwise sit flat/unstriped while every other column in
   the same row alternates. */
.coverage-table.table-striped > tbody > tr:nth-of-type(odd) > .row-number-col {
  background-color: rgba(0, 0, 0, 0.05);
}
thead .row-number-col { z-index: 2; background: var(--bg); }
.coverage-table th.sortable-col {
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
.coverage-table th.sortable-col:hover { color: var(--accent); }
.coverage-table th.sortable-col:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.coverage-table th.sortable-col::after {
  content: '\2195'; /* up-down arrow, unselected state */
  margin-left: 0.35em;
  color: var(--muted);
  font-size: 0.85em;
}
.coverage-table th.sortable-col[aria-sort="ascending"]::after { content: '\2191'; color: var(--fg); }
.coverage-table th.sortable-col[aria-sort="descending"]::after { content: '\2193'; color: var(--fg); }

/* --- /coverage's "Full jurisdiction detail table" filter row --- */
.coverage-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: center;
  margin: 1rem 0 0.5rem;
}
.coverage-filters input[type="search"] { max-width: 16rem; }
.coverage-filters select { max-width: 14rem; }
.coverage-filter-check {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.85rem;
  color: var(--muted);
  font-weight: 400;
}
.coverage-filter-count {
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 0.5rem;
}
.coverage-table .coverage-yesno-col {
  text-align: center;
  color: var(--muted);
  width: 1px;
  white-space: nowrap;
}

/* "Browse by state" link list on /coverage -- compact flex-wrapped rows
   instead of a tall one-per-line bullet list (up to ~40 states). */
.state-links {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0 1.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
}

/* Jurisdiction hub pages (/j/{slug}) -- breadcrumb trail, the per-body
   count line ("City Council (30) · Planning Commission (12)"), and the
   below-threshold note (see crud.JURISDICTION_HUB_MIN_INDEXABLE). */
.hub-breadcrumb { font-size: 0.9rem; margin-bottom: 0.5rem; }
.hub-breadcrumb a { text-decoration: none; }
.hub-breadcrumb a:hover { text-decoration: underline; }
.hub-bodies { color: var(--fg-muted, #555); margin-top: -0.25rem; }
.hub-body-count { opacity: 0.75; }
.hub-thin-note { font-style: italic; }
