/* /live/<slug> -- public shareable briefing page.

Design system: brand-locked to the production app's actual palette (the
same --bg/--ink/--purple values cc_v2.html uses), not a bespoke demo palette
-- this page is a growth surface, so it should look like CuriousCats, not
like world_intel.html's internal-tool styling.

Typography splits system chrome from editorial content, echoing "Samaritan
World Intelligence"'s own wire-dispatch framing: Space Mono (already used by
world_intel.html/cc_v2.html for machine-timestamped labels) stays the
"system voice" for the status stamp and time dividers; Space Grotesk -- a
genuine type-family companion to Space Mono, not an arbitrary pairing --
carries the "editorial voice" (the query headline, card headlines), used
only there, restrained everywhere else to plain system sans for legibility
and fast load. */

:root {
  --bg: #000000;
  --bg-elevated: #0d0d10;
  --card-border: rgba(255,255,255,0.08);
  --ink: #e4e2df;
  --ink-soft: rgba(228,226,223,0.6);
  --ink-faint: rgba(228,226,223,0.38);
  --purple: #a092e2;
  --purple-dim: rgba(160,146,226,0.35);
  --purple-bg: rgba(160,146,226,0.1);
  /* Text color for content sitting ON TOP of a solid --purple fill (e.g.
     .lb-sheet-btn--primary) -- needs its own token, not a hardcoded value,
     since --purple itself is a different shade per mode (a light pastel
     here, a darker saturated tone in light mode below) and dark text reads
     as WCAG-failing "black on medium purple" on that darker light-mode
     shade. Dark ink has ~7.3:1 contrast against this mode's --purple. */
  --purple-ink: #16121f;
  /* Download-bar background -- deliberately its OWN token, not --bg-elevated
     (which the bar used before): --bg-elevated is only one step lighter than
     --bg, so the bar read as barely distinguishable from the page behind it.
     A warm, more saturated tinted surface (not just a low-alpha --purple-bg
     wash, which blends back toward near-black at this low opacity) makes it
     read as a distinct banner, same effect as competitors' differently-hued
     download bars against their own dark pages. */
  --dlhook-bg: #241c3a;
  --lb-progress-fill: rgba(160,146,226,0.42);
  --x-blue: #1d9bf0;
  --x-blue-bg: rgba(29,155,240,0.16);
  --x-blue-border: rgba(29,155,240,0.35);
  --wire-red: #a3211f;
  --sep: rgba(255,255,255,0.14);
  --shadow: rgba(0,0,0,0.55);
  --mono: 'Space Mono', ui-monospace, 'SF Mono', monospace;
  --display: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
  --body-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* Per-card tint hue (live_briefing.js's applyHeroHue), set once each
     card's hero image resolves. 250 matches --purple's own hue, so an
     un-hydrated card looks neutral/on-palette rather than off, same
     reasoning cc_v2.html uses for its own --cc-card-hue default. */
  --lb-card-hue: 250;
}

* { box-sizing: border-box; }

/* Mobile Chrome/Safari paint a solid highlight color over a tapped link
   until the click resolves -- fine for a button, but reads as a stray
   text-selection-style highlight over a card's headline/insight text,
   which isn't how a native app (or this page's own modal/sheets) behaves. */
a { -webkit-tap-highlight-color: transparent; }

body {
  margin: 0;
  overflow-x: hidden;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--body-font);
  -webkit-font-smoothing: antialiased;
}

.lb-wrap {
  max-width: 640px;
  margin: 0 auto;
  padding: max(10px, env(safe-area-inset-top)) max(10px, env(safe-area-inset-right)) calc(48px + env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
}

.lb-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 22px; }
.lb-brand {
  display: inline-flex; align-items: center; gap: 8px;
  color: var(--ink); text-decoration: none;
  font-family: var(--mono); font-weight: 700; font-size: 13px;
  letter-spacing: 0.04em;
}
.lb-brand img { width: 22px; height: 22px; border-radius: 6px; }
.lb-share-btn {
  width: 32px; height: 32px; border-radius: 50%; border: 0; padding: 0; flex: none;
  background: var(--purple-bg); color: var(--ink-soft); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.lb-share-btn svg { width: 16px; height: 16px; }

/* The signature element: a wire-dispatch "status stamp" -- a bordered mono
   tag that reads as a dateline/classification marker, doubling as the
   freshness indicator every reload needs. The dot reuses the exact pulse
   ring already shipped on world_intel.html/cc_v2.html (a validated, tasteful
   bit of motion) rather than inventing new decoration. */
.lb-stamp {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--purple-dim);
  background: var(--purple-bg);
  border-radius: 999px;
  padding: 5px 12px 5px 10px;
  font-family: var(--mono); font-size: 10.5px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--purple);
  margin-bottom: 16px;
}
.lb-stamp-dot { position: relative; width: 7px; height: 7px; border-radius: 50%; background: var(--purple); flex: none; }
.lb-stamp-dot::after {
  content: ""; position: absolute; inset: -5px; border-radius: 50%;
  border: 1.5px solid var(--purple); animation: lbPulse 2.2s ease-out infinite;
}
@keyframes lbPulse {
  0%   { transform: scale(0.55); opacity: 0.9; }
  75%  { transform: scale(2.1); opacity: 0; }
  100% { transform: scale(2.1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lb-stamp-dot::after { animation: none; opacity: 0.5; }
}

.lb-h1 {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(1.6rem, 6.5vw, 2.35rem);
  line-height: 1.12;
  letter-spacing: -0.01em;
  margin: 0 0 0px;
}
.lb-h1-topic { text-transform: uppercase; }

/* /live/m/<slug>-only chrome: a collapsed one-liner that opens .lb-mon-sheet
   (read-only monitoring config), plus a horizontal-scroll filter row right
   below it. Absent (no markup rendered) on /live/<slug>, so these rules are
   dormant there. */
.lb-mon-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; background: none; border: none; border-bottom: 1px solid var(--card-border);
  color: var(--ink-soft); font-family: var(--body-font); font-size: 12.5px;
  padding: 10px 2px; margin: 14px 0 12px; cursor: pointer; text-align: left;
}
.lb-mon-chevron { flex: none; font-size: 13px; transition: transform 0.2s ease; }
.lb-mon-toggle[aria-expanded="true"] .lb-mon-chevron { transform: rotate(180deg); }

.lb-filter-row {
  display: flex; gap: 8px; overflow-x: auto; overscroll-behavior-x: contain;
  -ms-overflow-style: none; scrollbar-width: none;
  margin: 0 0 18px; padding: 2px 0 4px;
}
.lb-filter-row::-webkit-scrollbar { display: none; }
.lb-filter-chip {
  flex: none; display: inline-flex; align-items: center;
  font-family: var(--mono); font-size: 11px; font-weight: 700;
  letter-spacing: 0.03em; white-space: nowrap; text-decoration: none;
  border: 1px solid var(--card-border); background: var(--bg-elevated);
  color: var(--ink-soft); padding: 7px 13px; border-radius: 999px;
}
.lb-filter-chip--active { border-color: var(--purple-dim); background: var(--purple-bg); color: var(--purple); }
.lb-filter-chip-x { margin-left: 6px; font-weight: 700; }
/* Every configured chip is in the DOM from first paint (unpruned server-
   side -- see routes/monitoring_live_pages.py), so without this every load
   would flash the FULL list and then remove whichever ones turn out to have
   no live matches. The whole row -- not just the non-active chips -- stays
   hidden until it earns --ready (added once LB_ON_DATA_LOADED in
   monitoring_live.html knows real availability): a lone "All" pill sitting
   above an otherwise-empty, still-loading feed read as a stray, half-broken
   fragment on its own, not as "nothing to prune yet." .lb-filter-chip[hidden]
   below still does the real per-chip pruning once --ready lands. */
.lb-filter-row:not(.lb-filter-row--ready) { display: none; }

/* .lb-filter-chip's own display:inline-flex (an author rule) otherwise beats
   the [hidden] UA-stylesheet default, same reasoning as .lb-sheet-btn[hidden]
   above -- pruneable per chip (LB_ON_DATA_LOADED) and on the whole row
   (empty after pruning down to just "All"). */
.lb-filter-chip[hidden] { display: none; }
.lb-filter-row[hidden] { display: none; }

/* Coverage-mix line (buildCoverageHtml, live_briefing.js) -- plain
   body text under the h1, not another badge like .lb-stamp above it; the point
   is to read as a fact about this story, not more page chrome. */
.lb-coverage {
  display: flex; align-items: center; flex-wrap: wrap; gap: 5px;
  font-size: 13px; color: var(--ink-soft); margin: 6px 0 0;
}
.lb-coverage-item { display: inline-flex; align-items: center; gap: 4px; }
.lb-coverage-item svg { width: 13px; height: 13px; flex: none; }
.lb-coverage-dot { opacity: 0.5; }

.lb-chip {
  font-family: var(--mono); font-size: 10px; font-weight: 700;
  letter-spacing: 0.07em; text-transform: uppercase;
  border: 1px solid var(--card-border); background: var(--bg-elevated);
  color: var(--ink-soft); padding: 4px 10px; border-radius: 4px;
}

/* Sticky "TRENDING Xm ago" divider -- same dot+label+count+rule shape as
   world_intel.html's .time-divider, plus cc_v2.html's sticky positioning so
   a scrolling reader keeps context. */
.lb-divider {
    position: sticky;
    top: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 24px;
    
    width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    box-sizing: border-box;
    padding: 8px max(18px, env(safe-area-inset-right)) 8px max(18px, env(safe-area-inset-left));
    --lb-progress: 0%;
    background-color: var(--bg);
    background-image: linear-gradient(90deg, var(--lb-progress-fill) 0 calc(var(--lb-progress) - 2px), var(--purple) calc(var(--lb-progress) - 2px) var(--lb-progress), transparent var(--lb-progress) 100%);
    padding-left: 0;
    margin-left: -15px;
}
.lb-divider-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--purple); flex: none; }
.lb-divider.is-past .lb-divider-dot { background: var(--ink-faint); }
.lb-divider-label {
  font-family: var(--mono); font-size: 11px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--purple);
  white-space: nowrap;
}
.lb-divider.is-past .lb-divider-label { color: var(--ink-soft); }
.lb-divider-count { color: var(--ink-faint); font-weight: 400; }
.lb-divider-rule { flex: 1; height: 1px; background: linear-gradient(90deg, var(--sep), transparent); }

/* Loading ribbon -- shown while /data is in flight (JS toggles body.lb-loading,
   see live_briefing.js's fetch gate). Same gradient/--lb-progress language as
   .lb-divider above rather than a new loader component, just indeterminate
   (pulsing across the full width) since there's no real percent to report yet. */
body.lb-loading::before {
  content: '';
  position: fixed; top: 0; left: 0; right: 0; z-index: 10;
  height: 3px;
  background-image: linear-gradient(90deg,
    transparent, var(--lb-progress-fill), var(--purple), var(--lb-progress-fill), transparent);
  background-size: 60% 100%;
  background-repeat: no-repeat;
  animation: lbRibbonPulse 1.2s ease-in-out infinite;
}
@keyframes lbRibbonPulse {
  0% { background-position: -60% 0; }
  100% { background-position: 160% 0; }
}
@media (prefers-reduced-motion: reduce) {
  body.lb-loading::before { animation: none; background-position: 50% 0; }
}

.lb-timeline { display: flex; flex-direction: column; gap: 10px; }

/* Server-rendered placeholder cards, cleared the moment render() overwrites
   #lbTimeline's innerHTML with real data -- see the template's own comment.
   Hidden outright once loading has actually failed (body.lb-error) instead
   of shimmering forever with no content ever arriving; live_briefing.js's
   fetch .catch also writes a real message into #lbTimeline at that point. */
.lb-skel-card {
  position: relative; overflow: hidden;
  background: var(--bg-elevated); border: 1px solid var(--card-border);
  border-radius: 16px;
}
/* .lb-h1 has no bottom margin (see its own rule) -- without this the first
   skeleton card sits flush against the title. */
.lb-skel-card:first-child { margin-top: 12px; }
.lb-skel-media { aspect-ratio: 16 / 9; background: var(--card-border); }
.lb-skel-body { padding: 14px 16px 16px; }
.lb-skel-line { height: 12px; border-radius: 6px; background: var(--card-border); margin-top: 10px; }
.lb-skel-line--meta { width: 35%; height: 9px; margin-top: 12px; }
.lb-skel-line--title { width: 92%; height: 15px; }
.lb-skel-line--title-short { width: 55%; height: 15px; }
.lb-skel-card::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--shadow), transparent);
  transform: translateX(-100%);
  animation: lbSkelShimmer 1.6s ease-in-out infinite;
}
@keyframes lbSkelShimmer {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .lb-skel-card::after { animation: none; }
}
body.lb-error .lb-skel-card { display: none; }

.lb-card {
  display: block;
  background: var(--bg-elevated);
  border: 1px solid var(--card-border);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 16px 32px -20px var(--shadow);
  opacity: 0; transform: translateY(10px);
  animation: lbCardIn 0.4s ease forwards;
}
@media (prefers-reduced-motion: reduce) {
  .lb-card { opacity: 1; transform: none; animation: none; }
}
@keyframes lbCardIn { to { opacity: 1; transform: translateY(0); } }

.lb-card-media { display: block; position: relative; aspect-ratio: 16 / 9; background: var(--card-border); text-decoration: none; }
.lb-card-media img,
.lb-card-img { width: 100%; height: 100%; object-fit: cover; display: block; background-size: cover; background-position: center; }

/* Portrait-first for video cards -- most of this corpus is YouTube Shorts,
   so default to 9:16 and upgrade to 16:9 only once _lbDetectVideoOrientation
   (live_briefing.js) confirms a landscape source via the oar2.jpg probe.
   Ported from cc_v2.js's PR #216 orientation detection, minus its grid-
   reconciliation half. Every video is its own full-width standalone card
   now (mediaHtml sets this class unconditionally for any video). */
.lb-card-media--video { aspect-ratio: 4 / 5; }
.lb-card-media--video.lb-video-landscape { aspect-ratio: 16 / 9; }
.lb-card-media-fade {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 55%, rgba(0,0,0,0.35) 100%);
  pointer-events: none;
}
.lb-badge-breaking {
  position: absolute; top: 10px; left: 10px; z-index: 2;
  background: var(--wire-red); color: #fff;
  font-family: var(--mono); font-size: 10px; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  padding: 3px 9px; border-radius: 999px;
}
.lb-play-btn {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 42px; height: 42px; border-radius: 50%;
  background: var(--wire-red); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; box-shadow: 0 3px 14px rgba(0,0,0,0.5);
  border: 2px solid rgba(255,255,255,0.9);
}

/* Autoplay-in-view (live_briefing.js's markVideoPlaying, ported from
   static/js/mastery/search-overlay.js/cc_v2.js's identical pattern) -- the
   iframe fills the same box .lb-card-img's poster image sits in;
   pointer-events:none so taps fall through to the wrapping .lb-card-media
   and open the same story/video link a plain click always has.
   --hidden keeps a hot-preloaded iframe already loaded/decoded but not
   visible, so promoting it to "playing" doesn't restart a fresh load. */
.lb-video-iframe {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border: none; pointer-events: none;
  opacity: 1; transition: opacity 0.2s ease;
}
/* Cross-fades instead of a hard visibility cut (cc_v2.html's own
   .cc2-video-iframe/--hidden pair) -- the iframe underneath is already
   preloaded/decoded by the time this toggles (see the comment above), so
   the only thing this was ever hiding was a jarring instant swap, not load
   time. */
.lb-video-iframe--hidden { opacity: 0; }
.lb-card.lb-video-playing .lb-play-btn { display: none; }

/* Mute/unmute toggle (live_briefing.js's toggleSound) -- opposite corner
   from the play button, only shown once a tile is actually the one
   playing (a tile with no live audio has nothing to mute), same gate
   .lb-play-btn's own inverse rule uses above. */
.lb-sound-btn {
  display: none; position: absolute; left: 8px; bottom: 8px; z-index: 2;
  width: 30px; height: 30px; border-radius: 50%; border: 0; padding: 0;
  background: rgba(20,20,20,0.55); color: #fff;
  align-items: center; justify-content: center;
}
.lb-card.lb-video-playing .lb-sound-btn { display: flex; }
.lb-sound-btn .lb-sound-icon { width: 16px; height: 16px; pointer-events: none; }
.lb-sound-btn .lb-sound-icon--off { display: none; }
.lb-sound-btn.lb-sound-btn--muted .lb-sound-icon--on { display: none; }
.lb-sound-btn.lb-sound-btn--muted .lb-sound-icon--off { display: block; }

/* Same-event ML article clusters (story_cluster_key/story_cluster_title,
   stamped by monitoring_live_feed.py's _cluster_items_by_headline -- see
   live_briefing.js's articleClusterHtml). A horizontal scroll-snap strip
   of full-size .lb-card children, not a compact tile -- an article card
   carries far more content than a video thumbnail+title, so compressing
   it into a small tile would cut off the summary/chips that make the card
   worth showing at all. */
.lb-cluster-block { margin: 10px 0; }
.lb-cluster-label {
  font-family: var(--mono); font-size: 11px; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft);
  margin-bottom: 10px;
}
.lb-cluster-row {
  display: flex; gap: 14px;
  overflow-x: auto; -webkit-overflow-scrolling: touch; scroll-snap-type: x mandatory;
  scrollbar-width: none; padding-bottom: 2px;
}
.lb-cluster-row::-webkit-scrollbar { display: none; }
.lb-cluster-row .lb-card { flex: none; width: 280px; scroll-snap-align: start; }

/* Standalone-article carousel (live_briefing.js's applyCarouselToCard) --
   same scroll-snap mechanics as .lb-cluster-row above, one level of
   nesting deeper (slides within a card instead of cards within a row).
   Native scroll-snap, no touch handlers -- a horizontal swipe inside
   the track doesn't fire the slide's own <a> click, same drag-vs-click
   disambiguation the browser already gives every anchor. */
.lb-carousel-track {
  display: flex; flex-wrap: nowrap; width: 100%;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
  scrollbar-width: none; -ms-overflow-style: none;
}
.lb-carousel-track::-webkit-scrollbar { display: none; }
.lb-carousel-slide {
  display: flex; flex-direction: column; flex: none; width: 100%;
  scroll-snap-align: start; scroll-snap-stop: always;
  text-decoration: none; color: inherit;
}
/* Only non-first slides get this -- slide 0's .lb-card-body renders
   exactly like today's un-hydrated card (top-aligned, natural flow). */
.lb-carousel-slide-body--center { display: flex; flex-direction: column; justify-content: center; flex: 1; }
.lb-carousel-media-wrap { position: relative; }
.lb-carousel-slide-img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; display: block; }
.lb-carousel-caption { margin: 0; }

/* Real tappable arrow -- cc_v2's own carousel has none (pure swipe, its
   trailing "→" is decorative text only); this page is a growth/share
   surface where swipe-discoverability matters more than inside the app,
   so it gets a real control. Vertically centered on the image specifically
   (positioned within .lb-carousel-media-wrap, not the whole slide) so it
   holds still regardless of how tall a given slide's caption/meta is. */
.lb-carousel-arrow {
  position: absolute; top: 50%; right: 10px; transform: translateY(-50%); z-index: 2;
  width: 34px; height: 34px; border-radius: 50%; border: 0; padding: 0;
  background: rgba(20,20,20,0.55); color: #fff; font-size: 22px; line-height: 1;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
/* Flat, not the gradient .lb-card-body gets -- the footer isn't fading in
   from a photo the way a slide's own body is, it's a standalone strip
   below the track, so it just sits at the gradient's own END color
   throughout (mirrors cc_v2.html's .cc-carousel-footer). Light-mode
   override sits in the @media (prefers-color-scheme: light) block above.
   Dots left, share button right (space-between + horizontal padding
   matching .lb-card-body's own 16px) -- this is where a carousel-
   hydrated card's share button lives (see carouselCardInnerHtml's own
   comment): sitting on this already-persistent footer, rather than
   inside slide 0's own scrollable content, means it stays reachable
   regardless of which slide the reader has swiped to. */
.lb-carousel-footer {
  display: flex; align-items: center; justify-content: space-between; padding: 10px 16px;
  background-color: hsl(var(--lb-card-hue) 55% 14% / 0.55);
}
.lb-carousel-dots { display: flex; align-items: center; gap: 5px; }
.lb-carousel-dot {
  width: 5px; height: 5px; border-radius: 50%; background: hsl(var(--lb-card-hue) 10% 60% / 0.4);
  transition: background 0.15s, transform 0.15s;
}
.lb-carousel-dot.active { background: hsl(var(--lb-card-hue) 45% 62%); transform: scale(1.3); }

/* Slight per-card hue wash derived from the hero image's own dominant
   color (--lb-card-hue, set by live_briefing.js's applyHeroHue once the
   image resolves) -- mirrors cc_v2.html's .item-body tint. Light-mode
   override sits in the @media (prefers-color-scheme: light) block above. */
.lb-card-body {
  padding: 14px 16px 16px;
  background-image: linear-gradient(to bottom, transparent, hsl(var(--lb-card-hue) 55% 14% / 0.55));
}
.lb-card-meta { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-soft); margin-bottom: 8px; flex-wrap: wrap; }
/* Bottom-right on the same line as whatever footer row it's placed in
   (lb-card-footer-row below for a plain card, lb-carousel-footer's own
   dots row for a hydrated one) -- previously its own row (lb-card-actions,
   now gone), which added a full extra line of height to every card.
   Previously sat inline in .lb-card-meta up top before that, where it
   read as nearly invisible against the meta row's own low-contrast text. */
.lb-card-share-btn {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  border: 0; padding: 0; background: transparent; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
/* Same PNG glyph cc_v2.html's own cardActionsHtml share button uses
   (content:url(...), swapped for light mode above) instead of this
   button's previous inline SVG -- matches that page's icon exactly. */
.lb-card-share-icon { width: 17px; height: 17px; content: url('/static/images/share_dark.png'); }
/* Chips + share on one row -- chips wrap independently in their own
   inner flex; the button is a plain flex:none sibling pinned right via
   the row's own justify-content, not a per-button margin trick. Article-
   only (cardHtml's own gate): a video never reaches either. */
.lb-card-footer-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-top: 10px; }
.lb-fav-row { display: flex; align-items: center; }
.lb-fav {
  width: 15px; height: 15px; border-radius: 50%; object-fit: cover;
  border: 1.5px solid var(--bg-elevated); margin-left: -5px;
  font-size: 8px; color: #fff; background: #999;
  display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
}
.lb-fav:first-child { margin-left: 0; }

.lb-card-headline {
  display: block; font-family: var(--display); font-weight: 600;
  font-size: 20px; line-height: 1.3;
  color: var(--ink); text-decoration: none; margin-bottom: 6px;
  
}

.lb-card-insight {
  display: block; font-size: 13.5px; line-height: 1.45; color: var(--ink-soft); margin: 0;
  text-decoration: none;
}
.lb-insight-text {
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.lb-insight-more { color: var(--purple); font-weight: 600; }

/* Video cards: r.insight is just the channel name, not a summary -- a plain
   label, no line-clamp/"Read more" (nothing further to read). */
.lb-card-channel { font-size: 12.5px; color: var(--ink-faint); margin: 0; }

.lb-card-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }

/* Tweet cards -- deliberately NOT the article card shape (hero image +
   meta row + headline + insight). Ported from cc_v2.js's tweetCardHtml/
   tweetPreviewHtml (avatar + name/handle head, tweet text as the primary
   content, an X-blue tint instead of the neutral card background, and an
   attached-article link-preview sub-card when there is one) -- adapted to
   this page's own type system (Space Grotesk/Mono) rather than cc_v2's. */
/* Subtle hue wash layered under the existing X-blue tint, same mechanism
   as .lb-card-body above -- kept low-opacity so it doesn't
   fight the tweet card's own brand-blue background. This page's tweet
   cards sometimes carry an image (tweetMediaHtml) unlike cc_v2's, which
   skips hue extraction for tweets entirely since its own tweet cards have
   no image slot -- see live_briefing.js's applyHeroHue dispatch comment. */
.lb-tweet-card {
  background-color: var(--x-blue-bg);
  background-image: linear-gradient(to bottom, transparent, hsl(var(--lb-card-hue) 55% 14% / 0.35));
  border-color: var(--x-blue-border); padding: 16px;
}
.lb-tweet-head { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px; }
.lb-tweet-avatar {
  width: 36px; height: 36px; border-radius: 50%; object-fit: cover; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 700; font-size: 16px;
}
.lb-tweet-avatar-fallback { background: var(--x-blue); }
.lb-tweet-who { flex: 1; min-width: 0; line-height: 1.3; padding-top: 1px; }
.lb-tweet-name {
  display: block; font-family: var(--display); font-weight: 600; font-size: 14px;
  color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lb-tweet-handle { font-weight: 400; }
.lb-tweet-meta { display: block; font-size: 12px; color: var(--ink-soft); }
.lb-tweet-text {
  display: block; color: var(--ink); font-size: 15px; line-height: 1.5;
  text-decoration: none; margin-bottom: 2px;
}

.lb-tweet-media, .lb-tweet-article-preview {
  /* Overrides the base .lb-card-media rule's own aspect-ratio:16/9 (meant
     for a full-bleed article/video hero with nothing else inside it) --
     .lb-tweet-article-preview needs the OUTER link to size naturally so
     the caption panel below the image isn't clipped to the image's own
     height; .lb-card-img still gets the 16:9 treatment itself, below. */
  aspect-ratio: auto;
  display: block; margin-top: 10px; border-radius: 12px; overflow: hidden;
}
.lb-tweet-media .lb-card-img, .lb-tweet-article-preview .lb-card-img { aspect-ratio: 16 / 9; }
.lb-tweet-article-preview { border: 1px solid var(--card-border); background: var(--bg-elevated); }
.lb-tweet-article-info { display: flex; flex-direction: column; gap: 3px; padding: 8px 10px 10px; }
.lb-tweet-article-title {
  font-size: 12.5px; font-weight: 600; color: var(--ink); line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.lb-tweet-article-domain { font-size: 11px; color: var(--ink-soft); text-transform: lowercase; }

mark.lb-hl {
  background: var(--purple-bg); color: inherit; font-weight: inherit;
  border-radius: 3px; padding: 0 2px;
}

.lb-load-more { text-align: center; padding: 22px 0 6px; color: var(--ink-soft); font-size: 13px; }
.lb-load-more button {
  background: var(--purple-bg); border: 1px solid var(--purple-dim); color: var(--purple);
  font-family: var(--mono); font-size: 12px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  border-radius: 999px; padding: 9px 20px; cursor: pointer;
}

.lb-empty { color: var(--ink-soft); font-size: 14px; text-align: center; padding: 30px 0; }

.lb-footer {
  margin-top: 44px; padding-top: 20px; border-top: 1px solid var(--sep);
  text-align: center; color: var(--ink-faint); font-size: 12.5px;
}
.lb-footer a { color: var(--purple); text-decoration: none; font-weight: 600; }
/* Sits right after .lb-timeline in document order, so while the timeline is
   still empty (body.lb-loading -- see the ribbon comment above) this would
   otherwise render right under the h1/stamp instead of after real content,
   reading as a stray floating line rather than a footer. Hidden for that
   one beat instead of pinned to the viewport bottom, since this page's
   existing bottom-fixed slot (.lb-dlhook) is already spoken for and a
   second fixed bar stacked with it would crowd a page this narrow. */
body.lb-loading .lb-footer { display: none; }

/* Persistent bottom download nudge -- shown/hidden by scroll direction
   (updateDownloadHookVisibility in live_briefing.js), same translateY-off-
   the-bottom-edge idiom curiouscats_story.css's .story-sticky-cta uses,
   but on this page's own token system (not that page's fixed dark/cream
   palette) since this page has real light/dark support. z-index sits
   below .lb-modal/.lb-sheet (50/60) -- live_briefing.js hides this
   whenever either of those opens, so it never visually stacks under them. */
.lb-dlhook {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: flex; align-items: center; gap: 10px;
  background: var(--dlhook-bg); border-top: 1px solid var(--card-border);
  box-shadow: 0 -6px 24px var(--shadow);
  padding: 10px max(14px, env(safe-area-inset-right)) calc(10px + env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left));
  transform: translateY(0);
  transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
.lb-dlhook.lb-dlhook--hidden { transform: translateY(110%); }
.lb-dlhook-logo { width: 30px; height: 30px; border-radius: 8px; flex: none; }
.lb-dlhook-text { flex: 1; min-width: 0; margin: 0; font-size: 12.5px; line-height: 1.35; color: var(--ink-soft); }
.lb-dlhook-btn {
  flex: none; background: var(--purple); color: var(--purple-ink);
  font-family: var(--mono); font-size: 11.5px; font-weight: 700; letter-spacing: 0.03em;
  padding: 9px 14px; border-radius: 999px; text-decoration: none; white-space: nowrap;
}

/* Arm B (lbStickyVariant, live_briefing.js) -- same bar re-anchored to the
   top edge instead of the bottom, Ground-News-banner style. Border/shadow
   direction flips to match (bottom edge now, not top), safe-area-inset
   swaps to the top inset, and the show/hide transform inverts (off-screen
   is UP, not down) since this variant is dismiss-driven, not scroll-driven
   -- see updateDownloadHookVisibility's own early-return for Arm B. */
.lb-dlhook--top {
  top: 0; bottom: auto;
  border-top: none; border-bottom: 1px solid var(--card-border);
  box-shadow: 0 6px 24px var(--shadow);
  padding: 10px max(14px, env(safe-area-inset-right)) 10px max(14px, env(safe-area-inset-left));
  padding-top: calc(10px + env(safe-area-inset-top));
}
.lb-dlhook--top.lb-dlhook--hidden { transform: translateY(-110%); }
.lb-dlhook-dismiss {
  flex: none; background: none; border: none; color: var(--ink-soft);
  font-size: 20px; line-height: 1; cursor: pointer; padding: 4px 6px;
}

/* z-index 60+, ABOVE .lb-modal's 50 -- window.lbOpenVideoFromEmbed
   (live_briefing.js) can open this sheet while the article modal is
   already open (a video tapped inside that iframe's own article), and it
   needs to render visibly on top of that modal, not underneath it. */
.lb-sheet-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.55);
  opacity: 0; pointer-events: none; transition: opacity .2s ease; z-index: 60;
}
.lb-sheet-backdrop.open { opacity: 1; pointer-events: auto; }
.lb-sheet {
  position: fixed; left: 0; right: 0; bottom: 0;
  max-width: 640px; margin: 0 auto;
  background: var(--bg-elevated); border: 1px solid var(--card-border); border-bottom: none;
  border-radius: 16px 16px 0 0;
  padding: 18px 20px calc(20px + env(safe-area-inset-bottom));
  max-height: 90vh; max-height: 90dvh; overflow-y: auto; overscroll-behavior: contain;
  transform: translateY(100%); transition: transform .28s cubic-bezier(0.22, 0.61, 0.36, 1);
  z-index: 61; box-shadow: 0 -8px 30px var(--shadow);
}
.lb-sheet.open { transform: translateY(0); }
.lb-sheet-header { display: flex; align-items: center; justify-content: flex-end; margin-bottom: 6px; }
.lb-sheet-close { background: none; border: none; color: var(--ink-soft); font-size: 22px; line-height: 1; cursor: pointer; padding: 4px 8px; }
.lb-sheet-copy { color: var(--ink-soft); font-size: 13.5px; line-height: 1.5; margin: 0 0 14px; text-align: center; }
.lb-sheet-actions { display: flex; flex-direction: column; gap: 10px; }
.lb-sheet-btn { display: block; text-align: center; text-decoration: none; padding: 12px 16px; border-radius: 10px; font-size: 14px; font-weight: 600; }
.lb-sheet-btn--primary { background: var(--purple); color: var(--purple-ink); display: flex; align-items: center; justify-content: center; gap: 8px; }
.lb-sheet-btn-logo { width: 20px; height: 20px; border-radius: 5px; flex: none; }
.lb-sheet-btn:not(.lb-sheet-btn--primary) { background: transparent; border: 1px solid var(--card-border); color: var(--ink-soft); }
/* .lb-sheet-btn's own display:block (same specificity as the [hidden]
   UA-stylesheet rule) would otherwise win the cascade and keep a hidden
   button visible. */
.lb-sheet-btn[hidden] { display: none; }

/* .lb-mon-sheet reuses .lb-sheet/.lb-sheet-backdrop/.lb-sheet-header/
   .lb-sheet-close as-is (including .lb-sheet's own max-height/overflow-y
   scroll for a config with many sections) -- only the title layout and the
   read-only chip lists below are new. Own title class (not .lb-sheet-title,
   which .lb-sheet-header's justify-content: flex-end leaves unstyled on the
   video sheet above) since this header needs a real left-aligned heading. */
.lb-mon-sheet .lb-sheet-header { justify-content: space-between; margin-bottom: 14px; }
.lb-mon-sheet-title { font-family: var(--display); font-weight: 700; font-size: 16px; }
.lb-mon-section { margin-bottom: 16px; }
.lb-mon-section:last-child { margin-bottom: 0; }
.lb-mon-section-title {
  font-family: var(--mono); font-size: 10.5px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint);
  margin: 0 0 8px;
}
.lb-mon-chip-list { display: flex; flex-wrap: wrap; gap: 6px; }
.lb-mon-chip {
  font-family: var(--body-font); font-size: 12.5px;
  border: 1px solid var(--card-border); background: var(--bg);
  color: var(--ink); padding: 5px 11px; border-radius: 999px;
}
.lb-mon-chip--excluded { color: var(--ink-faint); text-decoration: line-through; }

/* Orientation-aware so the shared-element morph (animateVideoIntoSheet in
   live_briefing.js) never has to stretch a 9:16 thumbnail into a 16:9 box
   or vice versa -- matches the source card's own lb-video-landscape/
   portrait-default convention. Portrait is height-capped (not full device
   height) so the close control and download CTA below it stay reachable
   on a short phone screen; landscape just fills the sheet's own width. */
.lb-video-player { position: relative; margin: 0 0 16px; border-radius: 14px; overflow: hidden; background: #000; }
.lb-video-player.lb-video-player--landscape { width: 100%; aspect-ratio: 16 / 9; }
.lb-video-player.lb-video-player--portrait { width: auto; height: min(60vh, 580px); max-width: 100%; aspect-ratio: 9 / 16; margin-left: auto; margin-right: auto; }
.lb-video-player-inner { position: absolute; inset: 0; }
.lb-video-player-inner iframe { width: 100%; height: 100%; border: none; display: block; }

/* Top-right, not a bottom corner -- YouTube's own native control bar
   (controls=1, lbBuildEmbedUrl) occupies the full bottom edge of the
   player once shown, and a bottom-corner button here collided with it. */
.lb-video-popout-btn {
  position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; border-radius: 50%;
  background: rgba(0,0,0,0.55); color: #fff; border: none; padding: 0;
  display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2;
}
.lb-video-popout-btn svg { width: 16px; height: 16px; }

/* Clone of the tapped thumbnail, grown from its tile position into the
   sheet's video slot while the sheet itself slides up (animateVideoIntoSheet)
   -- z-index above both the backdrop (60) and the sheet (61) so it stays
   visible flying over them, landing exactly where the real player appears. */
.lb-video-clone { position: fixed; pointer-events: none; z-index: 62; background-size: cover; background-position: center; }

/* Reproduces static/mastery_css/mastery.css's own .timeline-article-modal/
   .article-modal-header look (same idiom templates/cc_v2.html and
   templates/world_intel.html's own equivalent already use).

   Unlike this page's other chrome, the colors below are hardcoded to
   curiouscats_story.css's own fixed dark palette (#0c0c14/#e4e2df/#a092e2)
   instead of this page's own var(--bg)/var(--ink) tokens. The iframed
   content (/story?embed=1) is that always-dark page with no light-mode
   variant at all; this page DOES support light mode, so using its own
   tokens here produced a jarring split -- a light header sitting directly
   above the iframe's permanently-dark content whenever a visitor's OS was
   in light mode. Matching the iframe's own fixed palette keeps the whole
   modal visually consistent regardless of this page's theme. */
.lb-modal {
  position: fixed; inset: 0; z-index: 50; overflow-y: auto;
  background: rgba(0,0,0,0.6); opacity: 0; pointer-events: none; transition: opacity .3s ease;
}
.lb-modal.active { opacity: 1; pointer-events: auto; }
.lb-modal-content {
  position: relative; width: 100%; max-width: 640px; height: 90vh; margin: 10vh auto 0;
  background: #0c0c14; display: flex; flex-direction: column; overflow: hidden;
  border-radius: 16px 16px 0 0; transform: translateY(100%);
  transition: transform .35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.lb-modal.active .lb-modal-content { transform: translateY(0); }
@media (min-width: 768px) { .lb-modal-content { border-radius: 16px; } }
.lb-modal-header {
  position: sticky; top: 0; z-index: 5; display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; background: #0c0c14; border-bottom: 1px solid rgba(255,255,255,0.08);
}
.lb-modal-back {
  background: none; border: 0; padding: 4px; color: #e4e2df; font-size: 20px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.lb-modal-title { flex: 1; min-width: 0; line-height: 1.2; margin: 5px 0; }
.lb-modal-owner {
  display: block; font-family: var(--mono); font-size: 10px; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: #a092e2;
}
.lb-modal-brief {
  display: block; font-size: 15px; font-weight: 700; color: #e4e2df;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lb-modal-header-btn {
  width: 32px; height: 32px; border-radius: 50%; border: 0; padding: 0;
  background: transparent; flex-shrink: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.lb-modal-header-icon { width: 18px; height: 18px; pointer-events: none; }
.lb-modal-frame { display: block; flex: 1; width: 100%; border: none; background: #0c0c14; }

/* 30s-dwell soft-gate popup (openDwellPopup/closeDwellPopup, live_briefing.js).
   z-index above .lb-sheet's 60/61 -- opening this doesn't yield to a video
   sheet the same way a video sheet yields to nothing above it, but in
   practice openDwellPopup skips firing entirely if either other overlay is
   already open, so this ordering is a "just in case," not load-bearing.
   Full-viewport opaque takeover (not a dimmed backdrop + partial sheet like
   the video/article overlays) -- covers the whole screen, same shape as the
   reference this was designed against. */
/* Two-region layout, not one flex-centered column: a scrollable body
   (skip link, notif preview, headline, checklist) plus a footer pinned to
   the bottom (lead-in line + CTA) that's ALWAYS reachable without
   scrolling, however tall the body content ends up being on a small
   device. Before this, the whole thing was one centered flex column, which
   read as "the CTA floats in the middle of the page" on a short viewport
   and could push the CTA below the fold entirely on a tall one. */
.lb-dwell {
  position: fixed; inset: 0; z-index: 70; background: var(--bg-elevated);
  display: flex; flex-direction: column;
  opacity: 0; pointer-events: none; transition: opacity 0.25s ease;
}
.lb-dwell.open { opacity: 1; pointer-events: auto; }
.lb-dwell-scroll {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; justify-content: center;
  padding: 12px 20px;
}
.lb-dwell-footer {
  flex: none; padding: 12px 20px calc(18px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--card-border);
}
/* Fixed top-left toolbar, not part of .lb-dwell-scroll's centered
   scrolling content -- stays in place regardless of how much the body
   scrolls, while keeping the original text-link look (not an icon-only
   chevron). */
.lb-dwell-toolbar { flex: none; padding: 20px 20px 0; }
.lb-dwell-back {
  background: none; border: none; font-family: var(--mono);
  font-size: 12px; font-weight: 700; color: var(--ink-soft); text-decoration: none;
  border-bottom: 1px solid var(--ink-soft); padding: 0 0 2px; cursor: pointer;
}
.lb-dwell-notif {
  display: flex; gap: 10px; align-items: flex-start; background: var(--card-border);
  border-radius: 14px; padding: 12px 14px; margin-bottom: 22px;
}
.lb-dwell-notif-icon { width: 30px; height: 30px; border-radius: 8px; flex: none; }
.lb-dwell-notif-app { font-family: var(--mono); font-size: 10px; font-weight: 700; color: var(--ink-soft); text-transform: uppercase; letter-spacing: 0.04em; }
.lb-dwell-notif-text { font-size: 13px; line-height: 1.4; color: var(--ink); margin-top: 3px; }
.lb-dwell-notif-text b { font-weight: 700; }
.lb-dwell-pulse {
  width: 7px; height: 7px; border-radius: 50%; background: #e0574f; flex: none;
  margin: 5px 0 0 auto; box-shadow: 0 0 0 3px rgba(224,87,79,0.25);
}
.lb-dwell-headline {
  font-family: var(--display); font-weight: 700; font-size: clamp(22px, 6.5vw, 28px);
  line-height: 1.22; color: var(--ink); margin: 0 0 8px; text-wrap: balance;
}
.lb-dwell-sub { font-size: 14px; line-height: 1.5; color: var(--ink-soft); margin: 0 0 18px; }
.lb-dwell-checklist { border: 1px solid var(--card-border); border-radius: 12px; padding: 12px 14px; margin-bottom: 22px; }
.lb-dwell-row { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; line-height: 1.4; color: var(--ink); margin-bottom: 9px; }
.lb-dwell-row:last-child { margin-bottom: 0; }
.lb-dwell-row svg { width: 15px; height: 15px; flex: none; margin-top: 1px; color: var(--purple); }
.lb-dwell-cta-lead { text-align: center; font-family: var(--mono); font-weight: 700; font-size: 12px; color: var(--ink); margin: 0 0 10px; }
.lb-dwell-cta {
  display: block; text-align: center; background: var(--purple); color: var(--purple-ink);
  font-family: var(--mono); font-weight: 700; font-size: 13px; letter-spacing: 0.02em;
  padding: 14px; border-radius: 999px; text-decoration: none;
}

/* Kept at the END of the file, not up near :root with the rest of the
   design-system comment -- every override below shares a selector with an
   unconditional rule declared earlier in this file (e.g. .lb-card-body at
   line ~442). Same specificity, so plain CSS cascade order decides the
   winner regardless of whether the media query matches; sitting here
   guarantees these always win in light mode instead of being silently
   shadowed by the later unconditional declaration (confirmed happening --
   .lb-chip's light override had zero effect until this block moved here). */
@media (prefers-color-scheme: light) {
  :root {
    --bg: #f7f7f7;
    --bg-elevated: #ffffff;
    --card-border: rgba(0,0,0,0.08);
    --ink: #201a12;
    --ink-soft: rgba(32,26,18,0.62);
    --ink-faint: rgba(32,26,18,0.4);
    --purple: #6f5fc4;
    --purple-dim: rgba(111,95,196,0.3);
    --purple-bg: rgba(111,95,196,0.08);
    /* This mode's --purple is darker/more saturated than dark mode's --
       dark ink on it (the dark-mode default above) drops to ~4.1:1
       contrast, under WCAG AA's 4.5:1 for normal text -- confirmed the
       actual bug reported (black text reading as "weird" on this purple).
       White clears ~5.1:1 here. */
    --purple-ink: #ffffff;
    --dlhook-bg: #efe9fb;
    --lb-progress-fill: rgba(111,95,196,0.35);
    --x-blue: #1d9bf0;
    --x-blue-bg: rgba(29,155,240,0.08);
    --x-blue-border: rgba(29,155,240,0.3);
    --sep: rgba(0,0,0,0.1);
    --shadow: rgba(0,0,0,0.12);
  }
  /* Lighter hue washes for light mode -- same reasoning as cc_v2.html's own
     body.light-mode overrides of these rules: the dark-mode gradient values
     read as near-black smudges against a light card. */
  .lb-card-body {
    background-image: linear-gradient(to bottom, transparent, hsl(var(--lb-card-hue) 30% 40% / 0.25));
  }
  .lb-tweet-card {
    background-image: linear-gradient(to bottom, transparent, hsl(var(--lb-card-hue) 30% 40% / 0.15));
  }
  .lb-carousel-footer {
    background-color: hsl(var(--lb-card-hue) 30% 40% / 0.25);
  }
  /* Same asset swap as cc_v2.html's own body.light-mode override -- share_dark.png
     is the glyph meant for a dark background (this page's default), so a light
     background needs the inverse asset, not just a color-var change. */
  .lb-card-share-icon { content: url('/static/images/share_light.png'); }
  /* Dark mode's opaque --bg-elevated chip fill reads fine because it's close
     in value to the card's own dark background either way; in light mode
     the same solid white pill stands out hard against the muted photo-hue
     gradient behind it. Blend it in instead of matching the card chrome. */
  .lb-chip {
    background: rgba(0,0,0,0.06);
    border-color: transparent;
  }
}
