/* SignalGuard — keyframe animations & gradient utilities
 *
 * Tailwind (CDN) handles 99% of styling. This file owns:
 *   - Keyframe animations (mesh drift, severity pulse, card stagger,
 *     button shimmer, grid breathe)
 *   - The hero grid + mesh background images (Tailwind CDN can't compile
 *     the custom backgroundImage tokens reliably across all builds, so
 *     we duplicate them here as utilities to be safe)
 *   - Reduced-motion overrides
 *
 * All other visual decisions live in DESIGN.md.
 */

/* --- Custom backgrounds (mirror tailwind.config.theme.extend.backgroundImage) --- */
.bg-mesh {
  background-image:
    radial-gradient(ellipse 80% 50% at 50% 0%, rgba(124, 156, 255, 0.18), transparent 60%),
    radial-gradient(ellipse 60% 40% at 80% 100%, rgba(91, 124, 255, 0.12), transparent 60%),
    radial-gradient(ellipse 50% 35% at 15% 80%, rgba(124, 156, 255, 0.08), transparent 60%);
}
.bg-grid {
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
  background-size: 48px 48px;
}

/* Hero submit button — hardcoded gradient + outline so it's always readable
   regardless of Tailwind CDN JIT state. */
.scan-submit {
  background: linear-gradient(135deg, #7C9CFF 0%, #5B7CFF 100%);
  color: #0A0B0F;
  border: 1px solid #A6BBFF;
  box-shadow:
    0 0 0 1px rgba(124, 156, 255, 0.35) inset,
    0 8px 24px -8px rgba(124, 156, 255, 0.55);
  letter-spacing: 0.01em;
}
.scan-submit:hover:not(:disabled) {
  background: linear-gradient(135deg, #94AEFF 0%, #7C9CFF 100%);
  border-color: #C2D0FF;
  box-shadow:
    0 0 0 1px rgba(124, 156, 255, 0.5) inset,
    0 10px 32px -8px rgba(124, 156, 255, 0.7);
  transform: translateY(-1px);
}
.scan-submit:active:not(:disabled) {
  transform: translateY(0);
}
.scan-submit:disabled {
  background: linear-gradient(135deg, #5B7CFF 0%, #4A6BE8 100%);
  border-color: #6B85D8;
  box-shadow: none;
}
.scan-submit svg, .scan-submit i {
  color: #0A0B0F;
  stroke: #0A0B0F;
}

/* Weather forecast strip — hardcoded grid + day-cell layout so it doesn't
   depend on Tailwind CDN compiling dynamic grid-cols / arbitrary classes. */
.wx-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.wx-day {
  flex: 1 1 0;
  min-width: 0;
  position: relative;
  text-align: center;
  padding: 10px 8px 12px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid #262936;
  overflow: hidden;
}
@media (max-width: 900px) {
  .wx-day { flex-basis: calc(25% - 8px); }
}
@media (max-width: 560px) {
  .wx-day { flex-basis: calc(33.333% - 8px); }
}
.wx-day::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, var(--temp-color, transparent) 0%, transparent 60%);
  opacity: 0.10;
  pointer-events: none;
}
.wx-day > * { position: relative; }
.wx-day-name {
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #6B6E7A;
  font-weight: 500;
}
.wx-day-icon {
  margin: 6px auto 4px;
  display: flex;
  justify-content: center;
  color: var(--temp-color, #A8ABB5);
}
.wx-day-icon svg, .wx-day-icon i {
  width: 24px;
  height: 24px;
}
.wx-day-temp {
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 700;
  font-size: 18px;
  line-height: 1.1;
  margin-top: 2px;
}
.wx-day-text {
  font-size: 11px;
  line-height: 1.3;
  color: #A8ABB5;
  margin-top: 4px;
  min-height: 28px;
}
.wx-day-wind {
  margin-top: 4px;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10px;
  color: #6B6E7A;
}

/* Scan-size chip active state */
.scan-chip[aria-pressed="true"] {
  background-color: rgba(124, 156, 255, 0.12);
  color: #E6E8EC;
  box-shadow: 0 0 0 1px rgba(124, 156, 255, 0.4) inset;
}

/* Hero vignette — fades hero content into the next section */
.hero-vignette {
  box-shadow: inset 0 -120px 120px -60px #0a0b0f;
}

/* Mask the hero grid so it fades top + bottom */
.hero-grid-mask {
  -webkit-mask-image: linear-gradient(to bottom, transparent, black 20%, black 80%, transparent);
          mask-image: linear-gradient(to bottom, transparent, black 20%, black 80%, transparent);
}

/* --- 1. Hero gradient mesh drift — slow, ambient --- */
@keyframes mesh-drift {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.9; }
  50%      { transform: translate3d(2%, -1%, 0) scale(1.05); opacity: 1; }
}
.animate-mesh-drift { animation: mesh-drift 18s ease-in-out infinite; }

/* --- 2. Critical severity pulse — restrained, not alarming --- */
@keyframes severity-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(229, 72, 77, 0.5); }
  50%      { box-shadow: 0 0 0 6px rgba(229, 72, 77, 0); }
}
.animate-severity-pulse { animation: severity-pulse 2.4s ease-in-out infinite; }

/* --- 3. Card entry stagger — applied to result cards --- */
@keyframes card-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.animate-card-in {
  animation: card-in 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

/* --- 4. Search button shimmer — runs on hover only --- */
@keyframes shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}
.btn-shimmer {
  position: relative;
  overflow: hidden;
}
.btn-shimmer::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(110deg, transparent 30%, rgba(255, 255, 255, 0.18) 50%, transparent 70%);
  background-size: 200% 100%;
  opacity: 0;
  transition: opacity 0.2s;
  pointer-events: none;
}
.btn-shimmer:hover::after {
  opacity: 1;
  animation: shimmer 1.4s linear infinite;
}

/* --- 5. Subtle grid breathe — for the hero grid backdrop --- */
@keyframes grid-breathe {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 0.7; }
}
.animate-grid-breathe { animation: grid-breathe 8s ease-in-out infinite; }

/* --- 6. Focus ring smooth-in for inputs --- */
.focus-ring-smooth {
  transition: box-shadow 0.25s ease, border-color 0.25s ease;
}

/* --- 7. AI reasoning chevron rotation --- */
.reasoning-toggle[aria-expanded='true'] .reasoning-chevron {
  transform: rotate(90deg);
}
.reasoning-chevron {
  transition: transform 0.2s ease;
}

/* --- 8. Drawer slide-down (event details) --- */
.drawer {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.drawer[data-open='true'] {
  grid-template-rows: 1fr;
}
.drawer > .drawer-inner {
  overflow: hidden;
  min-height: 0;
}
/* When the drawer is fully open, allow children (e.g. address-autocomplete
 * dropdown) to spill outside the drawer's bounds. The clip is only needed
 * for the collapse animation. */
.drawer[data-open='true'] > .drawer-inner {
  overflow: visible;
}

/* --- 9. Loading progress bar fill --- */
@keyframes progress-indeterminate {
  0%   { transform: translateX(-100%) scaleX(0.4); }
  50%  { transform: translateX(0%)    scaleX(0.6); }
  100% { transform: translateX(120%)  scaleX(0.4); }
}
.progress-bar-fill {
  transform-origin: left center;
  animation: progress-indeterminate 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

/* --- 10. Severity left-border colors (work around Tailwind CDN
 * sometimes choking on `border-l-severity-{x}` arbitrary tokens) --- */
.border-l-severity-low      { border-left-color: #3FB984; }
.border-l-severity-medium   { border-left-color: #E3B341; }
.border-l-severity-high     { border-left-color: #F08A4B; }
.border-l-severity-critical { border-left-color: #E5484D; }

/* --- 11. Address-autocomplete dropdown — explicit opaque background and
 * hover/selected states so the dropdown never bleeds into the elements
 * behind it (Tailwind CDN was failing to compile `bg-elevated` /
 * `hover:bg-surface` reliably for JS-injected list items). */
#event-location-suggestions {
  background-color: #111318 !important;
}
.location-suggest-item {
  background-color: #111318;
  transition: background-color 0.12s ease;
}
.location-suggest-item:hover,
.location-suggest-item[aria-selected='true'] {
  background-color: #16191F;
}

/* --- 12. Loading-state signal grid -----------------------------------------
 * The post-submit loader is a 17-row live progress grid (see #signal-grid in
 * index.html). Each row's visual state is driven by the `data-state`
 * attribute on the <li>, with optional `data-severity` on `loaded` rows.
 * Tailwind CDN can't compile attribute-driven utilities reliably, so we own
 * the rules here using the same hex tokens as the theme.
 */
:root {
  --sg-text-primary: #F5F7FA;
  --sg-text-secondary: #A8B0BD;
  --sg-text-muted: #6B7180;
  --sg-border: #1F232B;
  --sg-border-strong: #2A2F3A;
  --sg-accent: #7C9CFF;
  --sg-accent-glow: #5B7CFF;
  --sg-sev-low: #3FB984;
  --sg-sev-medium: #E3B341;
  --sg-sev-high: #F08A4B;
  --sg-sev-critical: #E5484D;
}

/* Row base — every row reserves space for the dot + meta even when empty
 * so the layout doesn't jitter as states transition. */
.signal-row { position: relative; transition: background-color 0.18s ease; }
.signal-row .signal-icon { color: var(--sg-text-muted); transition: color 0.18s ease; }
.signal-row .signal-label { color: var(--sg-text-secondary); transition: color 0.18s ease; }
.signal-row .signal-meta  { color: var(--sg-text-muted);    min-width: 0; }
.signal-row .signal-dot   { background-color: var(--sg-border-strong); transition: background-color 0.25s ease, box-shadow 0.25s ease; }

/* Pending: nothing happening yet. */
.signal-row[data-state="pending"] { opacity: 0.62; }

/* In-flight: subtle accent rail + pulsing dot. The icon brightens to accent
 * to signal "this signal is the one we're talking to right now." */
.signal-row[data-state="in-flight"] .signal-icon  { color: var(--sg-accent); }
.signal-row[data-state="in-flight"] .signal-label { color: var(--sg-text-primary); }
.signal-row[data-state="in-flight"] .signal-dot   {
  background-color: var(--sg-accent);
  box-shadow: 0 0 0 3px rgba(124,156,255,0.18);
  animation: sg-pulse 1.4s ease-in-out infinite;
}
.signal-row[data-state="in-flight"]::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 2px;
  background: linear-gradient(180deg, var(--sg-accent) 0%, var(--sg-accent-glow) 100%);
}

/* Loaded: severity color-locked. The dot color is the strongest signal —
 * label + icon stay neutral so the eye scans down a column of dots. */
.signal-row[data-state="loaded"] .signal-icon  { color: var(--sg-text-secondary); }
.signal-row[data-state="loaded"] .signal-label { color: var(--sg-text-primary); }
.signal-row[data-state="loaded"] .signal-meta  { color: var(--sg-text-secondary); }
.signal-row[data-state="loaded"][data-severity="low"]      .signal-dot { background-color: var(--sg-sev-low); }
.signal-row[data-state="loaded"][data-severity="medium"]   .signal-dot { background-color: var(--sg-sev-medium); }
.signal-row[data-state="loaded"][data-severity="high"]     .signal-dot { background-color: var(--sg-sev-high); }
.signal-row[data-state="loaded"][data-severity="critical"] .signal-dot { background-color: var(--sg-sev-critical); }
.signal-row[data-state="loaded"]:not([data-severity])      .signal-dot { background-color: var(--sg-text-muted); }

/* Quiet: signal returned but had nothing to report. Treated as a positive —
 * green dot, "quiet" meta, full opacity. Distinct from `loaded` w/ no severity
 * which is just unscored. */
.signal-row[data-state="quiet"] .signal-icon  { color: var(--sg-sev-low); }
.signal-row[data-state="quiet"] .signal-label { color: var(--sg-text-primary); }
.signal-row[data-state="quiet"] .signal-meta  { color: var(--sg-sev-low); }
.signal-row[data-state="quiet"] .signal-dot   { background-color: var(--sg-sev-low); }

/* Failed: muted with a strikethrough on the label. The reason text in
 * .signal-meta does the explaining ("rate limited", "unreachable", etc.). */
.signal-row[data-state="failed"] { opacity: 0.55; }
.signal-row[data-state="failed"] .signal-icon  { color: var(--sg-text-muted); }
.signal-row[data-state="failed"] .signal-label {
  color: var(--sg-text-muted);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  text-decoration-color: rgba(107,113,128,0.45);
}
.signal-row[data-state="failed"] .signal-meta  { color: var(--sg-text-muted); }
.signal-row[data-state="failed"] .signal-dot   {
  background-color: transparent;
  box-shadow: inset 0 0 0 1px var(--sg-border-strong);
}

@keyframes sg-pulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(124,156,255,0.18); transform: scale(1); }
  50%      { box-shadow: 0 0 0 5px rgba(124,156,255,0.28); transform: scale(1.08); }
}

/* Page chrome: dim/hide the Threat Brief title block + filter chips while
 * a scan is in flight. The DOM stays mounted so layout doesn't jump when
 * the scan completes — we just lower opacity and disable interaction.
 * The `flex` Tailwind class on #controls-row was overriding the HTML
 * `hidden` attribute, which is why filter chips were leaking through into
 * the loading state. Using a class-based hide via body[data-scan] is
 * specificity-stable. */
body[data-scan="loading"] #results-section > div > .flex.items-center.justify-between { opacity: 0.35; pointer-events: none; user-select: none; }
body[data-scan="loading"] #controls-row { display: none !important; }

/* --- 13. Tweet results — severity-tiered zones ---------------------------
 * Three iterations of "size cards by content" (full-width stacked,
 * 4-col waterfall, 3-col waterfall) all produced funky layouts. The
 * root cause was that flexbox-wrap is not masonry — when a row's
 * tallest card defines the row break point, every shorter card leaves
 * a vertical gap before the next row, and 1+1 in a 3-col row creates
 * a visible "missing tile" hole.
 *
 * The fix is structural, not visual: bucket the tweets by severity in
 * renderResults() and render each tier into its OWN uniform grid.
 *
 *   .zone-alert  → critical + high — full-width column, max readability
 *   .zone-watch  → medium          — 2-col grid, balanced reading
 *   .zone-noise  → low + unknown   — 3-col compact tile grid, dismiss-fast
 *
 * Each zone is internally uniform, so within-zone gaps can't happen.
 * The visual hierarchy now mirrors the user's actual task: triage,
 * skim, dismiss.
 */

.zone-alert,
.zone-watch,
.zone-noise {
  display: grid;
  gap: 0.75rem;
  align-items: start; /* expanded reasoning grows that one card only */
}

/* Alert zone: critical + high. Always one column — these tweets are the
 * headline of the brief and should read at full reading width. */
.zone-alert {
  grid-template-columns: 1fr;
}

/* Watch zone: medium. 2-col on tablet+, 1-col on mobile. */
.zone-watch {
  grid-template-columns: 1fr;
}
@media (min-width: 768px) {
  .zone-watch { grid-template-columns: repeat(2, 1fr); }
}

/* Noise zone: low + unknown. Dense 3-col compact tiles at lg+. The
 * compact card variant (.card--compact) hides the metrics row and
 * category tag, clamps the body to 3 lines, and uses smaller padding —
 * so each tile reads as a glance, not a deep dive. */
.zone-noise {
  grid-template-columns: 1fr;
}
@media (min-width: 768px) {
  .zone-noise { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .zone-noise { grid-template-columns: repeat(3, 1fr); }
}

/* Compact card variant — used in the noise zone. The DOM is identical to
 * a full-density card, only the visible chrome differs. Body clamps to 3
 * lines without a fade gradient (those read as bug-like). The handle row,
 * severity pill, and reasoning toggle all stay accessible — a security
 * pro can still drill in, but the default reads as "checked, dismissed". */
.card--compact .card-category-tag,
.card--compact .card-metrics-row {
  display: none;
}
.card--compact .tweet-body {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 13px;
  line-height: 1.45;
}
/* Lift the clamp when the user opens "Why AI flagged this" — they want
 * to read this one in detail. */
.card--compact:has(.reasoning-toggle[aria-expanded="true"]) .tweet-body {
  -webkit-line-clamp: unset;
  display: block;
  overflow: visible;
}

/* --- 14. Hero signals strip (marquee) -------------------------------------
 * Visual evidence for the "17 live signals fused into one brief" claim
 * directly above. Sits in the dead zone between hero and "How it works"
 * that previously read as ~600px of empty page. The strip is full-bleed
 * (escapes the hero's max-w container) so it scrolls edge-to-edge, with
 * a horizontal mask fading the pills into the hero vignette so the
 * marquee reads as ambient instrumentation rather than a stock ticker.
 *
 * Pause behavior:
 *   - hover anywhere on the strip pauses the scroll (so users can read)
 *   - prefers-reduced-motion stops it entirely (and the static row is
 *     still meaningful — same 17 pills visible).
 *
 * The 17 pills are duplicated once in the markup so the loop is seamless;
 * the track translates from 0% to -50% (one full set width) and resets.
 */
.signal-strip {
  position: relative;
  width: 100%;
}

.signal-strip-eyebrow {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  max-width: 1120px;
  margin: 0 auto 1rem;
  padding: 0 1.5rem;
}
.signal-strip-rule {
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, #2A2F3A, transparent);
}
.signal-strip-label {
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #6B7180;
  font-weight: 500;
  white-space: nowrap;
}

/* Edge-fade mask — the load-bearing detail. Without this, a marquee on
 * a dark page reads as a CNN ticker. With both edges dissolving into
 * the hero vignette, the strip feels like the hero's own atmosphere. */
.signal-strip-mask {
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 8%, #000 92%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0%, #000 8%, #000 92%, transparent 100%);
  overflow: hidden;
  width: 100%;
}

.signal-strip-track {
  display: flex;
  gap: 0.75rem;
  width: max-content;
  list-style: none;
  margin: 0;
  padding: 0 1rem;
  animation: signal-scroll 80s linear infinite;
  will-change: transform;
}
.signal-strip:hover .signal-strip-track {
  animation-play-state: paused;
}

.signal-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.875rem;
  border-radius: 999px;
  border: 1px solid #1F232B;
  background: rgba(17, 19, 24, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  white-space: nowrap;
  flex: 0 0 auto;
}
.signal-pill i,
.signal-pill svg {
  width: 14px;
  height: 14px;
  color: #A8B0BD;
  flex-shrink: 0;
}
.signal-pill span {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #A8B0BD;
  font-weight: 500;
}

@keyframes signal-scroll {
  0%   { transform: translateX(0); }
  100% { transform: translateX(-50%); } /* one full set width — track is duplicated */
}

/* --- Respect prefers-reduced-motion across the board --- */
@media (prefers-reduced-motion: reduce) {
  .animate-mesh-drift,
  .animate-severity-pulse,
  .animate-card-in,
  .animate-grid-breathe,
  .progress-bar-fill { animation: none !important; }
  .btn-shimmer::after { animation: none !important; }
  .drawer { transition: none !important; }
  .signal-row[data-state="in-flight"] .signal-dot { animation: none !important; }
  .signal-strip-track { animation: none !important; }
}

/* ===================================================================
   15. Ambient utilities — polish track
   Brand-safe ambient color, eyebrow components, and elevation motion.
   Used by /dashboard and selectively across interior pages. Canvas
   stays #0A0B0F; color lives on chrome, edges, and state.
   =================================================================== */

/* .ambient-hero — accent-tinted wash for page-header bands. Anchored
   top-left at 4-12% opacity #7C9CFF, masked so it fades into body. */
.ambient-hero { position: relative; isolation: isolate; background-color: #0A0B0F; }
.ambient-hero::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background-image:
    radial-gradient(ellipse 70% 45% at 20% 0%,  rgba(124,156,255,0.12), transparent 60%),
    radial-gradient(ellipse 55% 40% at 90% 10%, rgba(124,156,255,0.06), transparent 65%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 70%, transparent 100%);
  pointer-events: none;
}
.ambient-hero::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background-image:
    linear-gradient(rgba(255,255,255,0.018) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,0.018) 1px, transparent 1px);
  background-size: 56px 56px;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
  opacity: 0.6; pointer-events: none;
}

/* .ambient-panel — severity-neutral surface wash for KPI tiles and
   hero cards. Subtle elevation without re-tinting canvas. */
.ambient-panel {
  position: relative; isolation: isolate;
  background-color: #111318; border: 1px solid #1F232B; border-radius: 16px;
}
.ambient-panel::before {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
  background:
    radial-gradient(ellipse 90% 60% at 50% 0%, rgba(245,247,250,0.025), transparent 70%),
    linear-gradient(180deg, rgba(255,255,255,0.012), transparent 40%);
  pointer-events: none;
}
.ambient-panel::after {
  content: ""; position: absolute; left: 12%; right: 12%; top: 0; height: 1px;
  background: linear-gradient(90deg, transparent, #2A2F3A, transparent);
  pointer-events: none;
}

/* .ambient-rail — vertical accent hairline for right-rail sidebars. */
.ambient-rail { position: relative; }
.ambient-rail::before {
  content: ""; position: absolute; top: 8%; bottom: 8%; left: 0; width: 1px;
  background: linear-gradient(180deg, transparent, #2A2F3A 30%, #2A2F3A 70%, transparent);
}

/* --- Eyebrow component — mono cap label + dot + hairline rule. --- */
.eyebrow { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; }
.eyebrow-dot { width: 6px; height: 6px; border-radius: 999px; background: #7C9CFF; box-shadow: 0 0 8px rgba(124,156,255,0.55); flex-shrink: 0; }
.eyebrow-label { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: #A8B0BD; white-space: nowrap; }
.eyebrow-rule { flex: 1; height: 1px; background: linear-gradient(90deg, #2A2F3A, transparent); }
.eyebrow-meta { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: #6B7180; white-space: nowrap; }
.eyebrow--severity .eyebrow-dot { background: #E3B341; box-shadow: 0 0 8px rgba(227,179,65,0.45); }
.eyebrow--severity .eyebrow-label { color: #E3B341; }
.eyebrow--critical .eyebrow-dot { background: #E5484D; box-shadow: 0 0 8px rgba(229,72,77,0.55); }
.eyebrow--critical .eyebrow-label { color: #E5484D; }

/* --- Restrained elevation — used on interactive cards in feeds/grids. */
.card-rise { transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease; will-change: transform; }
.card-rise:hover {
  transform: translateY(-2px);
  border-color: #2A2F3A;
  box-shadow: 0 1px 0 rgba(255,255,255,0.03) inset, 0 12px 32px -16px rgba(0,0,0,0.6), 0 0 0 1px rgba(124,156,255,0.08);
}
.focus-glow:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px #0A0B0F, 0 0 0 3px rgba(124,156,255,0.45), 0 0 16px -4px rgba(124,156,255,0.35);
}

/* --- Signature flourish — slow hairline scan-line across page headers. */
.scanline-host { position: relative; overflow: hidden; }
.scanline-host::after {
  content: ""; position: absolute; left: -20%; right: -20%; top: 0; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(124,156,255,0.55) 50%, transparent);
  transform: translate3d(-100%, 0, 0);
  animation: sg-scanline 7s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  pointer-events: none; opacity: 0.7;
}
@keyframes sg-scanline {
  0%        { transform: translate3d(-100%, 0, 0); }
  60%, 100% { transform: translate3d(100%, 0, 0); }
}

/* --- Critical-only pulse ring — never on lower severities. */
@keyframes sg-crit-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(229, 72, 77, 0.55); }
  70%  { box-shadow: 0 0 0 6px rgba(229, 72, 77, 0); }
  100% { box-shadow: 0 0 0 0 rgba(229, 72, 77, 0); }
}
.sg-crit-pulse { animation: sg-crit-pulse 2.4s cubic-bezier(0.4, 0, 0.6, 1) infinite; will-change: box-shadow; }

/* --- Live-feed heartbeat dot — accent only. Goes static-gray when dead. */
@keyframes sg-heartbeat { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
.sg-live-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #7C9CFF; animation: sg-heartbeat 1.8s ease-in-out infinite; will-change: opacity; }
.sg-live-dot--dead { animation: none; opacity: 0.5; background: #6B7180; }

/* --- Severity-escalation flash — single-fire when a row jumps tiers. */
@keyframes sg-flash {
  0%   { transform: scale(1); filter: brightness(1); }
  30%  { transform: scale(1.06); filter: brightness(1.35); }
  100% { transform: scale(1); filter: brightness(1); }
}
.sg-flash { animation: sg-flash 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) 1; will-change: transform, filter; }

/* --- Row-stagger fade-in — for feeds streaming results. --- */
@keyframes sg-row-in {
  from { opacity: 0; transform: translate3d(0, 6px, 0); }
  to   { opacity: 1; transform: none; }
}
.sg-row-in { animation: sg-row-in 0.35s ease-out both; animation-delay: calc(var(--i, 0) * 40ms); }

/* AI synthesis loading indicator — used while /api/brief is in flight. */
@keyframes sg-spin {
  to { transform: rotate(360deg); }
}
@keyframes sg-pulse-dot {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.4; transform: scale(0.8); }
}

/* Reduced-motion fallbacks for the polish utilities. */
@media (prefers-reduced-motion: reduce) {
  .scanline-host::after { animation: none; opacity: 0; }
  .sg-crit-pulse        { animation: none; box-shadow: 0 0 0 2px rgba(229,72,77,0.4); }
  .sg-live-dot          { animation: none; opacity: 0.7; }
  .sg-flash             { animation: none; }
  .sg-row-in            { animation: none; opacity: 1; }
}

/* ===================================================================
   15b. Page-wash — body-level continuous ambient gradient
   For pages where a section-bounded .ambient-hero or .hero-vignette
   ends in a visible horizontal seam against the rest of the page.
   This applies a soft accent wash across the entire body that fades
   gracefully into bg-base over hundreds of pixels — no hard cutoff.
   Section-level backdrops (mesh, grid, scanline) still render on top.
   Used on /, /docs.
   =================================================================== */
body.page-wash {
  background-color: #0A0B0F;
  background-image:
    radial-gradient(ellipse 1400px 1100px at 20% 0%,  rgba(124,156,255,0.08), transparent 65%),
    radial-gradient(ellipse 1100px 900px  at 80% 200px, rgba(124,156,255,0.05), transparent 70%),
    radial-gradient(ellipse 1600px 1200px at 50% 50%, rgba(124,156,255,0.025), transparent 75%);
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-position: 20% 0%, 80% 200px, 50% 50%;
}

/* Mask the wash behind the navbar so it doesn't bleed into the top
   strip. Doesn't depend on Tailwind's bg-base utility being compiled
   by the CDN — explicit #0A0B0F so the navbar is always opaque on
   page-wash pages. Covers any direct-child <header> at the top of body. */
body.page-wash > header { background-color: #0A0B0F; }

/* ===================================================================
   16. Auth-mode visibility — hide marketing chrome from signed-in users
   Pages tag elements with data-auth="signed-out" (marketing/sales) or
   data-auth="signed-in" (in-app chrome). body[data-user-mode] is set
   by the auth bootstrap. Default state (no attribute) renders
   signed-out copy so prospects landing fresh see the full pitch.
   =================================================================== */
/* The attribute is set on <html> by an inline <head> script so there's
   no flash of marketing chrome for signed-in users (and no flash of
   in-app chrome for prospects) before JS bootstraps. */
html[data-user-mode="signed-in"] [data-auth="signed-out"] { display: none !important; }
html:not([data-user-mode="signed-in"]) [data-auth="signed-in"] { display: none !important; }

/* When signed in on /scan, collapse the marketing hero's padding +
   strip its mesh/grid backdrops so the form sits tight against the
   slim in-app hero above it (which has its own ambient-hero treatment). */
html[data-user-mode="signed-in"] .hero-vignette {
  padding-top: 0.5rem !important;
  padding-bottom: 0.75rem !important;
}
html[data-user-mode="signed-in"] .hero-vignette > [aria-hidden="true"] { display: none; }
