/* /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; }

/* "Full coverage" media grid (buildCoverageGridHtml, live_briefing.js) --
   #lbCoverage's richer sibling, shown instead of it once lbCoverageIsRich.
   Flat photo grid, deliberately NOT another .lb-card: a card-in-a-card/pill
   stack here read as busy against everything else on the page (first design
   pass, since replaced) -- one shared caption-bar device on every tile
   instead of separate badges/pills is the whole visual idea. */
.lb-cov-eyebrow {
  display: flex; align-items: center; gap: 6px; margin: 14px 0 8px 1px;
  font-family: var(--mono); font-size: 11.5px; font-weight: 700;
  letter-spacing: 0.07em; text-transform: uppercase; color: var(--purple);
}
.lb-cov-eyebrow svg { width: 12px; height: 12px; flex: none; }
.lb-cov-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-bottom: 22px; }
/* Odd tile count (buildCoverageGridHtml returns 2, 3, or 4 tiles, never
   more) -- 3 is the one case that leaves a half-empty row, so let the 3rd
   tile claim the full row instead. :nth-child(3):last-child means "the 3rd
   tile AND there is no 4th" -- a 4-tile grid's own 3rd tile keeps its
   normal half-width slot untouched. Own (much wider) aspect-ratio too --
   keeping the normal 4:3.3 ratio at double width roughly doubled the
   tile's height as well, which for the quote/sources tiles (short,
   vertically-centered content) was mostly empty space rather than a
   nice big photo the way it is for the two media tiles. */
.lb-cov-grid > .lb-cov-tile:nth-child(3):last-child { grid-column: 1 / -1; aspect-ratio: 2.4 / 1; }
.lb-cov-tile {
  position: relative; aspect-ratio: 4 / 3.3; border-radius: 16px; overflow: hidden;
  background: var(--bg-elevated); border: 0; padding: 0; display: block; width: 100%;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.lb-cov-tile-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Shared caption bar -- the one recurring device every tile uses instead of
   its own badge/pill; media tiles both point at the same combined
   video+photo gallery (see buildCoverageGridHtml), so they share this exact
   caption text/count too, not two different numbers for two "covers" of
   the same underlying thing. */
.lb-cov-cap {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 20px 10px 8px;
  background: linear-gradient(180deg, transparent, rgba(0,0,0,0.72) 60%);
  display: flex; align-items: center; gap: 5px;
  font-family: var(--mono); font-size: 11.5px; font-weight: 700; color: #fff;
}
.lb-cov-cap svg { width: 12px; height: 12px; flex: none; opacity: 0.85; }
/* Quiet translucent ring -- .lb-play-btn (the timeline's own video
   affordance) now shares this exact style (ported 2026-09-14, replacing an
   earlier saturated red disc that read as too alert/CTA-like); kept as its
   own class since the timeline card is full-width, not a half grid tile. */
.lb-cov-play {
  position: absolute; left: 10px; top: 10px; width: 30px; height: 30px; border-radius: 50%;
  background: rgba(10,8,14,0.38); border: 1.5px solid rgba(255,255,255,0.55);
  display: flex; align-items: center; justify-content: center;
}
.lb-cov-play svg { width: 11px; height: 11px; fill: #fff; margin-left: 1.5px; }
/* The one built/colored object in the grid -- everything else is
   photographic, this is the deliberate break in that rhythm. */
.lb-cov-tile--quote {
  background: radial-gradient(120% 140% at 8% 0%, #6b57c9, #372a6e 55%, #241a4d 100%);
  padding: 14px; display: flex; flex-direction: column; justify-content: space-between; color: #fff;
}
.lb-cov-quote-mark {
  font-family: Georgia, serif; font-size: 52px; line-height: 0.5; font-weight: 700;
  color: rgba(255,255,255,0.28); position: absolute; top: 4px; left: 8px;
}
.lb-cov-quote-text {
  position: relative; font-family: var(--display); font-weight: 600; font-size: 13px; line-height: 1.32;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; margin-top: 16px;
}
.lb-cov-tile--quote .lb-cov-cap { position: relative; background: none; padding: 8px 0 0; }
/* A subtle version of the quote tile's own tint -- pairs the two "data"
   tiles (quote/sources) as a family distinct from the two photo tiles,
   without competing with the quote tile's own bolder gradient for the
   "one colored object" role. Plain --bg-elevated alone (the original
   version) read as too empty next to the other 3 photo-filled tiles. */
.lb-cov-tile--sources {
  background: radial-gradient(130% 110% at 100% 100%, var(--purple-bg), var(--bg-elevated) 60%);
  padding: 14px; display: flex; flex-direction: column; justify-content: space-between;
}
/* Faint background glyph, same "one big outline shape bleeding off an
   edge" convention as .lb-cov-quote-mark above -- reuses
   LB_COVERAGE_ICON_ARTICLE (the same document icon the plain #lbCoverage
   line and this tile's own favicons row already imply) rather than
   drawing a new one. Placed first in the tile's own markup so the
   favicons/count text (normal flow, no z-index needed) paint over it. */
.lb-cov-sources-watermark {
  position: absolute; right: -20px; bottom: -20px; width: 110px; height: 110px;
  color: rgba(160,146,226,0.16); pointer-events: none;
}
.lb-cov-sources-watermark svg { width: 100%; height: 100%; }
/* Bigger than .lb-fav-row's normal 15px meta-row size -- there they're a
   quiet source-attribution detail on one article's own card; here they're
   the proof point the tile exists to show off, and they're deliberately
   DISTINCT outlets (distinctFavicons in live_briefing.js), not one
   article's own favicons list. The `+` (not a plain descendant re-override
   of .lb-fav) keeps clear of .lb-fav:first-child's own margin-left:0 reset,
   same specificity as a plain `.lb-cov-tile--sources .lb-fav` override
   would be and would otherwise win or lose depending on source order alone. */
.lb-cov-tile--sources .lb-fav { width: 26px; height: 26px; font-size: 12px; }
.lb-cov-tile--sources .lb-fav + .lb-fav { margin-left: -9px; }
/* Count + label as one line ("71 articles tracked") -- a separate small
   label line under a lone number read as too sparse next to the other 3
   photo-forward tiles, given the favicons themselves can't be sized up
   much further without crowding. */
.lb-cov-sources-count { font-family: var(--display); font-weight: 700; font-size: 15px; margin-top: 10px; }

/* ---- drill-down sheets (media grid / articles list / quotes list) ---- */
.lb-cov-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.6);
  opacity: 0; pointer-events: none; transition: opacity .22s ease; z-index: 40;
}
.lb-cov-backdrop.open { opacity: 1; pointer-events: auto; }
.lb-cov-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; height: 88vh; height: 88dvh;
  max-width: 640px; margin: 0 auto;
  background: var(--bg-elevated); border-radius: 20px 20px 0 0; box-shadow: 0 -20px 50px var(--shadow);
  transform: translateY(100%); transition: transform .28s cubic-bezier(.2,.8,.2,1);
  z-index: 41; display: flex; flex-direction: column; overflow: hidden; touch-action: none;
}
.lb-cov-sheet.open { transform: translateY(0); }
.lb-cov-sheet.dragging { transition: none; }
.lb-cov-sheet-handle { flex: none; display: flex; justify-content: center; padding: 10px 0 2px; cursor: grab; }
.lb-cov-sheet-handle span { width: 36px; height: 4px; border-radius: 99px; background: rgba(255,255,255,0.22); }
.lb-cov-sheet-head {
  flex: none; display: flex; align-items: center; justify-content: space-between;
  padding: 10px calc(20px + env(safe-area-inset-right)) 12px calc(20px + env(safe-area-inset-left));
  border-bottom: 1px solid var(--card-border);
}
.lb-cov-sheet-title { font-family: var(--display); font-weight: 700; font-size: 16px; }
.lb-cov-sheet-title small { display: block; font-family: var(--mono); font-weight: 400; font-size: 11px; color: var(--ink-faint); margin-top: 2px; letter-spacing: 0.03em; }
.lb-cov-sheet-close { width: 30px; height: 30px; border-radius: 50%; border: 0; background: var(--bg); color: var(--ink); display: flex; align-items: center; justify-content: center; cursor: pointer; flex: none; font-size: 18px; line-height: 1; }
.lb-cov-sheet-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; touch-action: pan-y; }

.lb-cov-media-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px; padding: 2px; }
.lb-cov-media-cell { position: relative; aspect-ratio: 1; background: var(--bg); border: 0; padding: 0; cursor: pointer; }
.lb-cov-media-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lb-cov-media-cell .lb-cov-play { width: 22px; height: 22px; left: 6px; top: 6px; }
.lb-cov-media-cell .lb-cov-play svg { width: 8px; height: 8px; }

.lb-cov-article-row {
  display: flex; gap: 12px; padding: 14px calc(20px + env(safe-area-inset-right)) 14px calc(20px + env(safe-area-inset-left));
  border-bottom: 1px solid var(--card-border); cursor: pointer; text-align: left; background: none; border-left: 0; border-right: 0; border-top: 0;
  width: 100%; color: inherit; font: inherit;
}
.lb-cov-article-row img { width: 64px; height: 64px; border-radius: 10px; object-fit: cover; flex: none; background: var(--bg); }
.lb-cov-article-headline { font-family: var(--display); font-weight: 600; font-size: 14.5px; line-height: 1.32; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 6px; }
.lb-cov-article-meta { display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 11px; color: var(--ink-soft); }
/* Smaller than .lb-fav's default 15px -- a dense row already carries a
   headline + source name + time on one line, favicons at the default size
   read as too heavy there. Scoped to this row's own meta line only -- the
   grid's own sources tile (.lb-cov-tile--sources above) keeps its bigger
   26px override untouched. */
.lb-cov-article-meta .lb-fav { width: 12px; height: 12px; font-size: 6px; }
.lb-cov-article-meta .lb-fav + .lb-fav { margin-left: -4px; }

.lb-cov-quote-row { padding: 14px calc(20px + env(safe-area-inset-right)) 14px calc(20px + env(safe-area-inset-left)); border-bottom: 1px solid var(--card-border); }
.lb-cov-quote-head { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
.lb-cov-quote-avatar {
  width: 32px; height: 32px; border-radius: 50%; flex: none; object-fit: cover;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-weight: 700; font-size: 12px; color: #fff;
}
.lb-cov-quote-name { font-weight: 700; font-size: 13.5px; }
.lb-cov-quote-handle { font-family: var(--mono); font-size: 11.5px; color: var(--ink-faint); }
.lb-cov-quote-row-text { font-size: 14px; line-height: 1.42; color: var(--ink); margin: 0 0 6px; text-decoration: none; display: block; }
.lb-cov-quote-time { font-family: var(--mono); font-size: 10.5px; color: var(--ink-faint); }

.lb-cov-sentinel { padding: 22px; text-align: center; font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
.lb-cov-spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--card-border); border-top-color: var(--purple); animation: lbCovSpin .7s linear infinite; display: inline-block; vertical-align: middle; margin-right: 7px; }
@keyframes lbCovSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .lb-cov-spinner { animation: none; } }

/* ---- level-2 full-screen photo viewer ---- */
.lb-cov-viewer { position: fixed; inset: 0; background: #000; z-index: 44; display: none; flex-direction: column; }
.lb-cov-viewer.open { display: flex; }
.lb-cov-viewer-bar { flex: none; display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; font-family: var(--mono); font-size: 12px; color: #fff; position: relative; z-index: 2; }
.lb-cov-viewer-close { width: 34px; height: 34px; border-radius: 50%; background: rgba(255,255,255,0.12); border: 0; color: #fff; font-size: 18px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.lb-cov-viewer-stage { position: relative; flex: 1; min-height: 0; display: flex; }
.lb-cov-viewer-track { flex: 1; display: flex; overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-x: contain; cursor: grab; }
.lb-cov-viewer-track.dragging { scroll-snap-type: none; cursor: grabbing; }
.lb-cov-viewer-slide { flex: 0 0 100%; scroll-snap-align: center; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; }
.lb-cov-viewer-slide img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform .25s ease; transform-origin: center; }
.lb-cov-viewer-track.dragging .lb-cov-viewer-slide img { transition: none; }
/* Transform itself (translate + scale) is driven entirely from JS
   (covApplyZoomTransform) once panning is involved -- a static CSS
   transform here can only ever express the un-panned, centered state.
   touch-action:none is the real reason this class exists in CSS at all:
   without it, a touch-drag meant to pan the zoomed image also scrolls the
   track's own native overflow-x, which is exactly the "panning flips to
   the next/prev photo" bug this fixes. */
.lb-cov-viewer-slide img.zoomed { touch-action: none; }
.lb-cov-viewer-slide img.panning { transition: none; }
.lb-cov-viewer-nav {
  position: absolute; top: 50%; transform: translateY(-50%); width: 38px; height: 38px; border-radius: 50%;
  background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.22); color: #fff;
  display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2;
}
.lb-cov-viewer-nav svg { width: 15px; height: 15px; }
.lb-cov-viewer-nav--prev { left: 12px; }
.lb-cov-viewer-nav--next { right: 12px; }
.lb-cov-viewer-nav.is-edge { visibility: hidden; }
.lb-cov-viewer-cap { flex: none; padding: 14px 20px calc(22px + env(safe-area-inset-bottom)); font-family: var(--mono); font-size: 11.5px; color: rgba(255,255,255,0.7); }
.lb-cov-viewer-cap b { color: #fff; font-family: var(--display); display: block; font-size: 13.5px; font-weight: 600; margin-bottom: 3px; }
.lb-cov-viewer-title { cursor: pointer; text-decoration: underline; text-decoration-color: rgba(255,255,255,0.3); text-underline-offset: 2px; }
/* Download-conversion slide (insertCovPromoSlideAt/startCovPromoRotation,
   live_briefing.js) -- not a blocking overlay, just one more real slide in
   the same swipeable track. Composed like .lb-video-blocked (icon/title/
   copy, centered column) rather than treated as a full-bleed photo: these
   static/images/sell-*.png screenshots are landscape marketing crops
   (1000x600) designed to be seen whole, so the visual box below is sized
   to their own aspect ratio -- same technique curiouscats_story.html's own
   download popup uses (.story-popup-slide/makeSlideshow) -- rather than
   cropped to fill the slide's full portrait height. */
.lb-cov-viewer-slide--promo {
  flex-direction: column; gap: 22px; padding: 6px 26px 28px; text-align: center;
}
.lb-cov-viewer-promo-visual {
  position: relative; width: 100%; max-width: 340px; aspect-ratio: 10 / 6;
  border-radius: 14px; overflow: hidden; box-shadow: 0 10px 34px rgba(0,0,0,0.55); flex: none;
}
.lb-cov-viewer-promo-visual img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transform: scale(1.045); transition: opacity 0.6s ease, transform 0.6s ease;
}
.lb-cov-viewer-promo-visual img.active { opacity: 1; transform: scale(1); }
.lb-cov-viewer-promo-copy { display: flex; flex-direction: column; gap: 6px; }
.lb-cov-viewer-promo-title {
  position: relative; min-height: 24px; margin: 0;
  font-family: var(--display); font-weight: 700; font-size: 19px; line-height: 1.25; color: #fff;
}
.lb-cov-viewer-promo-sub {
  position: relative; min-height: 20px; margin: 0 auto; max-width: 280px;
  font-size: 13.5px; line-height: 1.45; color: rgba(255,255,255,0.7);
}
.lb-cov-viewer-promo-title span, .lb-cov-viewer-promo-sub span {
  position: absolute; inset: 0; opacity: 0; transition: opacity 0.5s ease;
}
.lb-cov-viewer-promo-title span.active, .lb-cov-viewer-promo-sub span.active { position: relative; opacity: 1; }
.lb-cov-viewer-promo-cta {
  display: flex; align-items: center; justify-content: center; width: 100%; max-width: 300px;
  background: var(--purple); color: var(--purple-ink); text-decoration: none;
  padding: 12px 16px; border-radius: 10px; font-size: 14px; font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .lb-cov-backdrop, .lb-cov-sheet, .lb-cov-viewer-slide img,
  .lb-cov-viewer-promo-visual img, .lb-cov-viewer-promo-title span, .lb-cov-viewer-promo-sub span { transition: none; }
}

.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: rgba(10,8,14,0.38); border: 1.5px solid rgba(255,255,255,0.55);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 14px rgba(0,0,0,0.35);
}
.lb-play-btn svg { width: 15px; height: 15px; fill: #fff; margin-left: 2px; }

/* 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; }

/* Fallback state for a tile handleVideoUnavailable (live_briefing.js) has
   already given up on -- keeps the card's own already-loaded .lb-card-img
   thumbnail in place (only the iframe/play/sound controls are gone) rather
   than deleting the card, with a short explanation + a popout link back to
   YouTube (e.g. for a visitor who could still watch it there via a VPN).
   Fixed white-on-black like .lb-play-btn/.lb-sound-btn above, not
   var(--ink) -- these sit on the thumbnail photo, not the page surface. */
.lb-card-media--unavailable .lb-play-btn,
.lb-card-media--unavailable .lb-sound-btn { display: none; }
.lb-video-unavailable-tag {
  position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 2;
  background: rgba(10,8,14,0.72); color: #fff; font-size: 12px; font-weight: 600;
  line-height: 1.35; padding: 6px 10px; border-radius: 8px;
}
.lb-video-unavailable-popout {
  position: absolute; top: 10px; right: 10px; z-index: 2;
  width: 28px; height: 28px; border-radius: 50%;
  background: rgba(10,8,14,0.55); color: #fff;
  display: flex; align-items: center; justify-content: center;
}
.lb-video-unavailable-popout svg { width: 14px; height: 14px; }

/* 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; }

/* Shown instead of .lb-video-player once covVideoBlocked (live_briefing.js,
   openCovVideoBlockSheet) -- the actual tapped video's own thumbnail,
   blurred+dimmed, behind the message (same idea as the app's own
   sell-2.png promo: a real, recognizable video frame reads as much richer
   than an icon floating in an empty tinted box, which is what this looked
   like before). Text sits on a photo, not this page's own background, so
   it uses fixed white/rgba like .lb-cov-viewer-cap does for the same
   reason -- the adaptive --ink/--ink-soft tokens are calibrated against
   the page's own light/dark surface, not an arbitrary dimmed photo. A
   fixed 4:5 box, not orientation-matched to the real video.
   [hidden] override: .lb-video-blocked's own `display:flex` is the exact
   same specificity as the [hidden] UA-stylesheet rule and sits later in
   this file, so without this override toggling the `hidden` PROPERTY from
   JS had no visible effect at all -- confirmed as a real, shipped bug
   (stayed visible over every video, not just a blocked one). Same fix
   already applied to .lb-sheet-btn[hidden] above for the identical reason. */
.lb-video-blocked { position: relative; overflow: hidden; margin: 0 0 16px; border-radius: 14px; aspect-ratio: 4 / 5; background: #000; }
.lb-video-blocked[hidden] { display: none; }
.lb-video-blocked-thumb {
  position: absolute; inset: 0; background-size: cover; background-position: center;
  filter: blur(16px) brightness(0.5); transform: scale(1.15); /* scale hides the blur's own soft edge */
}
.lb-video-blocked-overlay {
  position: relative; height: 100%; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center; gap: 10px; padding: 24px;
}
.lb-video-blocked-icon { width: 48px; height: 48px; border-radius: 12px; }
.lb-video-blocked-title { font-weight: 700; font-size: 16px; color: #fff; margin: 0; }
.lb-video-blocked-copy { font-size: 13.5px; color: rgba(255,255,255,0.75); margin: 0; }

/* 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; overscroll-behavior: contain;
  background: rgba(0,0,0,0.6); opacity: 0; pointer-events: none; transition: opacity .3s ease;
}
.lb-modal.active { opacity: 1; pointer-events: auto; }
/* Full height, not a 90vh sheet with a peeking backdrop above it --
   opened from inside the photo viewer this can already be a 3rd layer
   (Media sheet -> viewer -> this modal), where a rounded floating sheet
   on top of an already edge-to-edge black viewer read as visually messy.
   Edge-to-edge everywhere (mobile and desktop alike) matches the viewer's
   own full-screen treatment instead of introducing a 3rd distinct overlay
   shape on top of the sheet/viewer pair. */
.lb-modal-content {
  position: relative; width: 100%; max-width: 640px; height: 100vh; height: 100dvh; margin: 0 auto;
  background: #0c0c14; display: flex; flex-direction: column; overflow: hidden;
  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); }
.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;
}

/* "Before you go" explainer overlay (openExplainer/closeExplainer,
   live_briefing.js) -- the shared next step for every ambient/low-
   commitment CTA on this page (the dwell popup's own continue above, the
   header brand link, the sticky download hook, the coverage-viewer promo
   slide). z-index 80, above the dwell popup's own 70 -- this can open ON
   TOP of the dwell popup (or the coverage viewer) rather than closing it
   first, same "stack, don't replace" precedent as the article modal
   opening on top of the coverage viewer (see overlayStack's own comment).
   Alternating light/dark sections, same device and exact colors as
   curiouscats_v2.html's own .before-after-section
   (static/curiouscats_v2.css, "Light / Dark alternating sections") --
   #0d0b18/#a092e2/#1e1a2a for dark, #F5F3EF/#6c5fc7 for light -- rather
   than inventing a new palette, since that alternating pattern already
   exists and is exactly what this asked to mirror. Deliberately not
   theme-aware (no @media prefers-color-scheme override, unlike the rest
   of this file) -- same as the section it mirrors, which doesn't adapt to
   the visitor's OS theme either. */
.lb-explainer {
  position: fixed; inset: 0; z-index: 80; background: #0d0b18; color: #fff;
  display: flex; flex-direction: column;
  opacity: 0; pointer-events: none; transition: opacity 0.25s ease;
}
.lb-explainer.open { opacity: 1; pointer-events: auto; }

.lb-explainer-topbar { flex: none; padding: 16px 20px; background: #0d0b18; border-bottom: 1px solid #1e1a2a; }
.lb-explainer-back {
  background: none; border: none; font-family: var(--mono); font-size: 12px; font-weight: 700;
  color: rgba(255,255,255,0.6); border-bottom: 1px solid rgba(255,255,255,0.6); padding: 0 0 2px; cursor: pointer;
}

.lb-explainer-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }

.lb-explainer-section { padding: 32px 20px 36px; }
.lb-explainer-section--dark {
  background: #0d0b18;
  --xpl-ink: #ffffff; --xpl-ink-soft: rgba(255,255,255,0.6); --xpl-ink-faint: rgba(255,255,255,0.3);
  --xpl-accent: #a092e2; --xpl-accent-ink: #1b1330; --xpl-surface: #13101d; --xpl-border: #1e1a2a;
}
.lb-explainer-section--light {
  background: #F5F3EF;
  --xpl-ink: #0d0b18; --xpl-ink-soft: rgba(13,11,24,0.55); --xpl-ink-faint: rgba(13,11,24,0.32);
  --xpl-accent: #6c5fc7; --xpl-accent-ink: #ffffff; --xpl-surface: #ffffff; --xpl-border: rgba(13,11,24,0.1);
}
.lb-explainer-section + .lb-explainer-section { border-top: 1px solid var(--xpl-border); }

.lb-explainer-eyebrow { font-family: var(--mono); font-size: 10.5px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: var(--xpl-accent); margin: 0 0 10px; }
.lb-explainer-h1 { font-family: var(--display); font-weight: 700; font-size: clamp(21px, 6vw, 24px); line-height: 1.18; letter-spacing: -0.01em; margin: 0 0 8px; color: var(--xpl-ink); }
.lb-explainer-h2 { font-family: var(--display); font-weight: 700; font-size: 17px; line-height: 1.26; margin: 0 0 18px; color: var(--xpl-ink); }
.lb-explainer-sub { font-size: 13px; line-height: 1.5; color: var(--xpl-ink-soft); margin: 0 0 20px; }
.lb-explainer-sub b { color: var(--xpl-ink); font-weight: 700; }

.lb-explainer-rotator { position: relative; width: 100%; aspect-ratio: 5/3; border-radius: 14px; overflow: hidden; border: 1px solid var(--xpl-border); background: var(--xpl-surface); box-shadow: 0 14px 34px rgba(0,0,0,0.28); }
.lb-explainer-rotator img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 0.5s ease; }
.lb-explainer-rotator img.active { opacity: 1; }
.lb-explainer-dots { display: flex; gap: 6px; margin-top: 12px; }
.lb-explainer-dot { width: 16px; height: 3px; border-radius: 2px; background: var(--xpl-border); border: 0; padding: 0; cursor: pointer; transition: background 0.25s; }
.lb-explainer-dot.active { background: var(--xpl-accent); }

.lb-explainer-rating-row { display: flex; align-items: baseline; gap: 10px; margin: 2px 0 6px; }
.lb-explainer-rating-num { font-family: var(--display); font-weight: 700; font-size: 36px; letter-spacing: -0.01em; color: var(--xpl-ink); }
.lb-explainer-rating-stars { color: #e8b84b; font-size: 15px; letter-spacing: 1px; }
.lb-explainer-rating-caption { font-family: var(--mono); font-size: 10.5px; color: var(--xpl-ink-faint); margin: 0 0 20px; }

.lb-explainer-quote { background: var(--xpl-surface); border: 1px solid var(--xpl-border); border-radius: 16px; padding: 18px 18px 16px; margin-bottom: 16px; }
.lb-explainer-quote-mark { font-family: 'Fraunces', Georgia, serif; font-style: italic; font-size: 32px; color: var(--xpl-accent); line-height: 0.4; display: block; margin-bottom: 6px; }
.lb-explainer-quote-text { font-family: 'Fraunces', Georgia, serif; font-style: italic; font-weight: 500; font-size: 15.5px; line-height: 1.45; color: var(--xpl-ink); margin: 0 0 12px; }
.lb-explainer-quote-source { font-family: var(--mono); font-size: 10.5px; color: var(--xpl-ink-faint); }

.lb-explainer-mini-quotes { display: flex; gap: 10px; }
.lb-explainer-mini-quote { flex: 1; background: var(--xpl-surface); border: 1px solid var(--xpl-border); border-radius: 12px; padding: 12px; font-size: 10.5px; line-height: 1.4; color: var(--xpl-ink-soft); }
.lb-explainer-mini-source { display: block; margin-top: 7px; font-family: var(--mono); font-size: 9px; color: var(--xpl-ink-faint); }
.lb-explainer-feature-name { display: block; font-family: var(--mono); font-weight: 700; font-size: 10px; letter-spacing: 0.03em; color: var(--xpl-accent); margin-bottom: 5px; }

.lb-explainer-proof-img { width: 100%; display: block; margin-top: 16px; border-radius: 4px; }
.lb-explainer-proof-cap { font-size: 12.5px; line-height: 1.5; color: var(--xpl-ink-soft); margin: 10px 0 0; }

.lb-explainer-notif { display: flex; align-items: center; gap: 11px; background: var(--xpl-surface); border: 1px solid var(--xpl-border); border-radius: 16px; padding: 12px 14px; margin: 4px 0 0; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }
.lb-explainer-notif-icon { width: 34px; height: 34px; border-radius: 9px; flex: none; background: linear-gradient(160deg, var(--xpl-accent), #6a5bc4); display: flex; align-items: center; justify-content: center; font-family: var(--display); font-weight: 700; font-size: 15px; color: var(--xpl-accent-ink); }
.lb-explainer-notif-body { flex: 1; min-width: 0; }
.lb-explainer-notif-app { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: 0.04em; color: var(--xpl-ink-faint); text-transform: uppercase; }
.lb-explainer-notif-text { font-size: 12.5px; line-height: 1.35; color: var(--xpl-ink); margin-top: 2px; }
.lb-explainer-notif-pulse { width: 7px; height: 7px; border-radius: 50%; background: var(--xpl-accent); flex: none; }

.lb-explainer-closing-quote { margin-top: 16px; }
.lb-explainer-closing-brand { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 22px; }
.lb-explainer-closing-mark { display: block; width: 30px; height: 30px; border-radius: 8px; object-fit: cover; }
.lb-explainer-closing-wordmark { font-family: var(--display); font-weight: 700; font-size: 15px; color: var(--xpl-ink); }

.lb-explainer-footer {
  flex: none; padding: 12px 20px calc(16px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid #1e1a2a;
  /* Same low-alpha wash as the CTA's own gradient below, just faint --
     testing whether the two together still reads clean or starts to feel
     busy. Low enough alpha that the white hint text stays legible either
     way. */
  background: linear-gradient(135deg, rgba(160,146,226,0.22) 0%, rgba(232,184,75,0.16) 100%), #0d0b18;
}
.lb-explainer-cta-hint { font-family: var(--mono); font-weight: 700; font-size: 12px; color: #fff; text-align: center; margin: 0 0 10px; }
.lb-explainer-cta {
  display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 14px 0; border-radius: 13px;
  /* Deliberately not the same flat --purple as the dwell popup's own CTA
     right before this screen -- back to back, two identical purple
     buttons on a dark ground read as "the tap didn't do anything." A
     gold-warmed gradient keeps it in the same accent family (the rating
     stars already use this gold) while looking like a distinct, live
     control. */
  background: linear-gradient(135deg, #a092e2 0%, #e8b84b 100%);
  color: #1b1330; font-family: var(--display); font-weight: 700; font-size: 15px; border: 0; text-decoration: none;
}
.lb-explainer-cta:active { transform: scale(0.985); }

/* 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;
  }
}
