/* ─────────────────────────────────────────────────────────────────────
   Loryn Visual Vocabulary
   ─────────────────────────────────────────────────────────────────────
   Codified primitives + tokens. Source of truth for every render
   decision. Composes per principles in docs/visual-constitution.md.

   Anytime a surface needs a void backdrop, atmospheric depth, weighted
   typography, material emergence, cinematic motion — it MUST pull from
   here, not invent its own one-off CSS. New primitives go here.

   File is loaded BEFORE journal.css so journal.css can override
   primitives only when truly necessary (and that override should be
   considered a constitution failure to be backed out later).
   ───────────────────────────────────────────────────────────────────── */

:root {
  /* ── Color tokens ─────────────────────────────────────────────────
     Premium ambient palette. The blue is warm-desaturated to avoid
     pure-channel SaaS-blue (#7ab8f5) which bands hard on dark
     backgrounds. The void colors deepen with depth (top is slightly
     warmer / closer, bottom is cooler / further). */
  --vv-void-near:        #050507;   /* near-black, neutral (was #0a0c14 — a
                                       blue-grey that lifted the whole field
                                       blue; that was the persistent "still
                                       blue") */
  --vv-void-mid:         #030305;   /* middle plane — near-black */
  --vv-void-far:         #020204;   /* furthest plane — near-black, neutral */
  --vv-ambient-blue:     220 80% 70%;   /* hsl — for ambient light pools */
  --vv-ambient-warm:     210 50% 75%;   /* slightly warmer for highlights */
  --vv-cyan-accent:      #7ab8f5;       /* Loryn brand light, used like punctuation */
  --vv-alarm-red:        #d04848;
  --vv-warn-amber:       #b08f4a;
  --vv-text-primary:     rgba(232, 240, 255, 0.96);
  --vv-text-secondary:   rgba(232, 240, 255, 0.68);
  --vv-text-muted:       rgba(232, 240, 255, 0.42);
  --vv-text-quiet:       rgba(232, 240, 255, 0.24);

  /* ── Typography tokens ────────────────────────────────────────────
     Letterspacing is the difference between "calculator output" and
     "Patek dial readout." Numerals use tabular-num + tighter
     letterspacing. Labels use wide tracking. */
  --vv-font-display:     -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif;
  --vv-font-numerals:    -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif;
  --vv-font-label:       -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif;

  /* Hero numeral — the headline number on a surface. Patek dial weight. */
  --vv-hero-size:        64px;
  --vv-hero-weight:      250;
  --vv-hero-tracking:    -0.02em;
  --vv-hero-leading:     1.04;

  /* Stat numeral — a supporting stat. */
  --vv-stat-size:        28px;
  --vv-stat-weight:      300;
  --vv-stat-tracking:    -0.01em;
  --vv-stat-leading:     1.1;

  /* Body — prose, narration */
  --vv-body-size:        15px;
  --vv-body-weight:      400;
  --vv-body-tracking:    0;
  --vv-body-leading:     1.55;

  /* Label — uppercase metadata above stats/sections */
  --vv-label-size:       10px;
  --vv-label-weight:     700;
  --vv-label-tracking:   0.22em;
  --vv-label-color:      var(--vv-text-muted);

  /* Micro — caption, footnote, sub-line under a hero */
  --vv-micro-size:       11px;
  --vv-micro-weight:     500;
  --vv-micro-tracking:   0.06em;
  --vv-micro-color:      var(--vv-text-muted);

  /* ── Depth / material tokens ──────────────────────────────────────
     Surfaces emerge from the void via ambient glow + soft shadow. */
  --vv-glow-soft:        0 0 32px 0 rgba(122, 184, 245, 0.06);
  --vv-glow-medium:      0 0 56px 0 rgba(122, 184, 245, 0.09);
  --vv-shadow-soft:      0 8px 32px 0 rgba(0, 0, 0, 0.4);
  --vv-shadow-deep:      0 20px 64px 0 rgba(0, 0, 0, 0.6);
  --vv-edge-glass:       inset 0 1px 0 0 rgba(255, 255, 255, 0.04);

  /* ── Motion tokens ────────────────────────────────────────────────
     Cinematic = slow + considered + bezier (never linear). */
  --vv-ease-emerge:      cubic-bezier(0.16, 1, 0.3, 1);
  --vv-ease-cinematic:   cubic-bezier(0.4, 0, 0.1, 1);
  --vv-ease-breath:      cubic-bezier(0.45, 0, 0.55, 1);

  --vv-dur-quick:        320ms;
  --vv-dur-considered:   560ms;
  --vv-dur-cinematic:    900ms;
  --vv-dur-breath:       14s;
  --vv-dur-drift:        72s;

  /* ── Spacing rhythm ───────────────────────────────────────────────
     Considered ladder. Tighter at the surface level, generous in void. */
  --vv-space-hair:       2px;
  --vv-space-tight:      6px;
  --vv-space-snug:       10px;
  --vv-space-comfort:    16px;
  --vv-space-roomy:      24px;
  --vv-space-generous:   40px;
  --vv-space-vast:       80px;
}

/* ─────────────────────────────────────────────────────────────────────
   PRIMITIVE: .void-backdrop
   ─────────────────────────────────────────────────────────────────────
   The ambient void treatment. Apply to any container that should feel
   like "you are inside the chamber." Replaces ad-hoc gradient stacks
   on pseudo-elements scattered throughout journal.css.

   Layers (z-order, back to front):
     1. Base — three-stop deep void gradient (far → mid → near)
        with a soft directional ambient pool from above-center
     2. Ambient pulse — two large radial pools that breathe on a slow
        cycle. Multi-stop interpolation curves with extended tails
        (no hard cutoffs). Multiple pools at different scales create
        depth grammar (far-soft vs mid-defined).
     3. Drift haze — slowest layer, very subtle, drifts on long
        cycles to give the void "weather"
     4. Grain — SVG fractal noise overlay at very low opacity.
        Breaks 8-bit gradient banding invisibly. THIS IS THE FIX for
        the "pixely / cheap" appearance — gradients on dark bg always
        band; grain dithers them into smoothness.

   The grain is inlined as a data:image/svg+xml (no extra HTTP) and
   uses feTurbulence with high baseFrequency for fine static, then
   feColorMatrix to mute it to grey, then feComponentTransfer to drop
   the alpha. Sized to tile naturally.

   Variants:
     .void-backdrop                 — default cinematic breath
     .void-backdrop.still           — no motion (for non-chamber voids)
     .void-backdrop.intense         — denser ambient (used in chamber)
     .void-backdrop.quiet           — minimal ambient (used in modals)
   ───────────────────────────────────────────────────────────────────── */

.void-backdrop {
  position: relative;
  background:
    radial-gradient(ellipse 110% 75% at 50% 0%,
      hsla(var(--vv-ambient-warm), 0.06) 0%,
      hsla(var(--vv-ambient-warm), 0.02) 25%,
      transparent 55%),
    linear-gradient(180deg,
      var(--vv-void-near) 0%,
      var(--vv-void-mid)  55%,
      var(--vv-void-far)  100%);
  isolation: isolate;
  overflow: hidden;
}

/* Scrollable variant — for marketing / settings / onboarding pages
   where content overflows the viewport and must be reachable. The
   ::before / ::after ambient pools still render; they fix to the
   viewport so they don't visually scroll with the content. Without
   this modifier, body.void-backdrop's overflow:hidden silently swallows
   anything below the fold (welcome-cutoff bug 2026-06-07). */
.void-backdrop.scrollable {
  overflow: visible;
  min-height: 100vh;
}
.void-backdrop.scrollable::before,
.void-backdrop.scrollable::after {
  position: fixed;
  inset: 0;
}

/* Layer 2 — ambient pools that breathe. Two radials at different
   scales for depth grammar. Multi-stop interpolation prevents banding
   visible on retina, with long soft tails (no hard cutoffs). */
.void-backdrop::before {
  content: '';
  position: absolute;
  inset: -10%;
  background:
    radial-gradient(ellipse 60% 55% at 25% 78%,
      hsla(var(--vv-ambient-blue), 0.085) 0%,
      hsla(var(--vv-ambient-blue), 0.055) 14%,
      hsla(var(--vv-ambient-blue), 0.028) 32%,
      hsla(var(--vv-ambient-blue), 0.012) 52%,
      hsla(var(--vv-ambient-blue), 0.004) 72%,
      transparent 100%),
    radial-gradient(ellipse 70% 65% at 78% 22%,
      hsla(var(--vv-ambient-blue), 0.055) 0%,
      hsla(var(--vv-ambient-blue), 0.035) 16%,
      hsla(var(--vv-ambient-blue), 0.018) 36%,
      hsla(var(--vv-ambient-blue), 0.008) 58%,
      transparent 100%),
    radial-gradient(ellipse 45% 35% at 50% 105%,
      hsla(var(--vv-ambient-warm), 0.05) 0%,
      hsla(var(--vv-ambient-warm), 0.025) 25%,
      hsla(var(--vv-ambient-warm), 0.01) 55%,
      transparent 100%);
  pointer-events: none;
  z-index: -2;
  opacity: 0;
  animation: vvVoidBreath var(--vv-dur-breath) var(--vv-ease-breath) infinite;
}

/* Layer 3 — drift haze (very slow lateral drift, distant) */
.void-backdrop::after {
  content: '';
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(ellipse 50% 50% at 15% 30%,
      hsla(var(--vv-ambient-blue), 0.032) 0%,
      hsla(var(--vv-ambient-blue), 0.016) 25%,
      hsla(var(--vv-ambient-blue), 0.006) 55%,
      transparent 100%),
    radial-gradient(ellipse 40% 60% at 85% 75%,
      hsla(var(--vv-ambient-warm), 0.024) 0%,
      hsla(var(--vv-ambient-warm), 0.012) 30%,
      hsla(var(--vv-ambient-warm), 0.005) 60%,
      transparent 100%);
  pointer-events: none;
  z-index: -3;
  opacity: 0;
  animation: vvVoidDrift var(--vv-dur-drift) var(--vv-ease-breath) infinite;
}

/* Grain layer — the dither that kills banding. Tiles ~200px so the
   noise pattern doesn't visibly repeat. Opacity is critical: too high
   and you see grain (cheap), too low and banding shows through. */
.void-backdrop > .vv-grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: -1;
  opacity: 0.06;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.92' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.95 0 0 0 0 0.95 0 0 0 0 1 0 0 0 0.5 0'/></filter><rect width='200' height='200' filter='url(%23n)'/></svg>");
  background-size: 200px 200px;
  mix-blend-mode: overlay;
}

@keyframes vvVoidBreath {
  0%   { opacity: 0; transform: translate(0, 0); }
  18%  { opacity: 0.55; }
  50%  { opacity: 1; transform: translate(-1.5%, 0.8%); }
  100% { opacity: 0.55; transform: translate(0, 0); }
}

@keyframes vvVoidDrift {
  0%   { opacity: 0; transform: translate(0, 0); }
  10%  { opacity: 0.7; }
  50%  { opacity: 1; transform: translate(2.5%, -1.5%); }
  100% { opacity: 0.7; transform: translate(0, 0); }
}

/* Variants */
.void-backdrop.still::before,
.void-backdrop.still::after {
  animation: none;
  opacity: 0.8;
}
/* Chamber: desaturate the ambient pools (was 220 80% 70% — vivid SaaS-blue that
   made Command read "all blue" vs the journal). Keeps the pools breathing (the
   cinematic life) but as deep blue-grey depth, not a saturated wash. The orb's
   own --vv-cyan-accent glow stays the vivid blue life source. Chamber-scoped;
   modals (.quiet) and still voids keep the original. */
/* Chamber field = the journal's flat near-black (var(--vv-void-far) = #020205),
   NOT the default void gradient whose #0a0c14 top added a permanent blue lift.
   This is the "still blue, not black like the journal" fix — the floor itself
   was blue. The orb is the only luminous element; the ambient pools are kept
   but heavily backed off (desaturated + the breathing layer barely shows). */
.void-backdrop.intense {
  /* Flatten the void gradient to the journal's black for the chamber. The
     gradient's TOP token (--vv-void-near #0a0c14) was a blue-grey — that was
     the last blue. Override the gradient's input tokens to void-far so the
     gradient resolves to flat #020205 (bulletproof — changes the gradient's
     inputs rather than fighting the cascade), plus a flat bg fallback. */
  --vv-void-near: var(--vv-void-far);
  --vv-void-mid:  var(--vv-void-far);
  background: var(--vv-void-far) !important;
}
/* Kill the breathing blue ambient pools entirely in the chamber — they were
   STILL painting (and pulsing) blue over the flat floor. Black like the
   journal = no ambient pools. The orb is the only light. */
.void-backdrop.intense::before,
.void-backdrop.intense::after { display: none !important; }
.void-backdrop.quiet::before { animation-duration: 22s; }
.void-backdrop.quiet::after { animation-duration: 110s; }

/* ─────────────────────────────────────────────────────────────────────
   PRIMITIVE: .vv-emerge
   ─────────────────────────────────────────────────────────────────────
   Material treatment — surfaces that "emerge from the void." Use on
   cards, hero panels, modal frames. Composes with .void-backdrop on
   parent containers.

   Adds: soft ambient glow at edges + considered shadow + faint edge
   highlight. Surface sits *in* the chamber rather than *on* it.
   ───────────────────────────────────────────────────────────────────── */

.vv-emerge {
  position: relative;
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.018) 0%,
    rgba(255, 255, 255, 0.010) 50%,
    rgba(255, 255, 255, 0.006) 100%);
  box-shadow: var(--vv-shadow-soft), var(--vv-glow-soft), var(--vv-edge-glass);
  border-radius: 14px;
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

.vv-emerge.deep {
  box-shadow: var(--vv-shadow-deep), var(--vv-glow-medium), var(--vv-edge-glass);
}

/* ─────────────────────────────────────────────────────────────────────
   PRIMITIVE: typography composite classes
   ─────────────────────────────────────────────────────────────────────
   Drop-in classes that bundle the right font/weight/tracking/leading.
   Use these instead of one-off CSS. */

.vv-hero {
  font-family: var(--vv-font-numerals);
  font-size: var(--vv-hero-size);
  font-weight: var(--vv-hero-weight);
  letter-spacing: var(--vv-hero-tracking);
  line-height: var(--vv-hero-leading);
  font-variant-numeric: tabular-nums;
  color: var(--vv-text-primary);
}

.vv-stat {
  font-family: var(--vv-font-numerals);
  font-size: var(--vv-stat-size);
  font-weight: var(--vv-stat-weight);
  letter-spacing: var(--vv-stat-tracking);
  line-height: var(--vv-stat-leading);
  font-variant-numeric: tabular-nums;
  color: var(--vv-text-primary);
}

.vv-body {
  font-family: var(--vv-font-display);
  font-size: var(--vv-body-size);
  font-weight: var(--vv-body-weight);
  letter-spacing: var(--vv-body-tracking);
  line-height: var(--vv-body-leading);
  color: var(--vv-text-primary);
}

.vv-label {
  font-family: var(--vv-font-label);
  font-size: var(--vv-label-size);
  font-weight: var(--vv-label-weight);
  letter-spacing: var(--vv-label-tracking);
  text-transform: uppercase;
  color: var(--vv-label-color);
}

.vv-micro {
  font-family: var(--vv-font-label);
  font-size: var(--vv-micro-size);
  font-weight: var(--vv-micro-weight);
  letter-spacing: var(--vv-micro-tracking);
  color: var(--vv-micro-color);
}

/* ─────────────────────────────────────────────────────────────────────
   PRIMITIVE: .vv-cinematic-enter
   ─────────────────────────────────────────────────────────────────────
   Drop on any element you want to "emerge" rather than snap in. Slow
   fade + slight scale + drift up. Used as base for surface entries.
   ───────────────────────────────────────────────────────────────────── */

.vv-cinematic-enter {
  animation: vvCinematicEnter var(--vv-dur-cinematic) var(--vv-ease-emerge) both;
}

@keyframes vvCinematicEnter {
  0%   { opacity: 0; transform: translateY(8px) scale(0.985); filter: blur(2px); }
  60%  { opacity: 0.9; filter: blur(0); }
  100% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
}

/* ─────────────────────────────────────────────────────────────────────
   PRIMITIVE: .vv-ambient-pulse
   ─────────────────────────────────────────────────────────────────────
   Replaces all `animation: pulse 0.5s linear infinite` style loading
   indicators with a slow ambient breath. The "Loryn is processing"
   feedback. */

.vv-ambient-pulse {
  animation: vvAmbientPulse 2.8s var(--vv-ease-breath) infinite;
}

@keyframes vvAmbientPulse {
  0%, 100% { opacity: 0.6; }
  50%      { opacity: 1; }
}

/* ─────────────────────────────────────────────────────────────────────
   PRIMITIVE: .vv-briefing
   ─────────────────────────────────────────────────────────────────────
   Ambient failure briefing — the visible alternative to "silent return"
   anywhere a feature can't complete. Drops into the top-right of any
   parent (or the viewport via .vv-briefing.toast positioning), fades
   in via cinematic enter, lingers, fades out on dismiss/timeout.

   Variants:
     .vv-briefing.alarm   — red accent for actual failures
     .vv-briefing.signal  — cyan accent for non-error notices
     .vv-briefing.muted   — quiet grey for low-priority info
     .vv-briefing.toast   — fixes to bottom-right viewport corner

   Constitution principle 8: Loryn never stays silent on a failure.
   This primitive is how every failure surfaces.

   Companion JS helper: window.LorynBrief.show(text, opts) — see
   loryn.js for the global helper that creates + auto-dismisses these.
   ───────────────────────────────────────────────────────────────────── */
.vv-briefing {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  gap: var(--vv-space-hair);
  padding: var(--vv-space-snug) var(--vv-space-comfort);
  background: linear-gradient(180deg,
    rgba(255,255,255,0.024) 0%,
    rgba(255,255,255,0.010) 100%);
  border-left: 1px solid rgba(122,184,245,0.4);
  border-radius: 0 8px 8px 0;
  box-shadow: var(--vv-shadow-soft), var(--vv-glow-soft), var(--vv-edge-glass);
  max-width: 340px;
  animation: vvCinematicEnter var(--vv-dur-considered) var(--vv-ease-emerge) both;
}

.vv-briefing.alarm  { border-left-color: var(--vv-alarm-red); }
.vv-briefing.signal { border-left-color: rgba(122,184,245,0.55); }
.vv-briefing.muted  { border-left-color: rgba(255,255,255,0.18); }

.vv-briefing .vv-brief-label {
  font-family: var(--vv-font-label);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--vv-text-muted);
}
.vv-briefing.alarm  .vv-brief-label { color: var(--vv-alarm-red); }
.vv-briefing.signal .vv-brief-label { color: var(--vv-cyan-accent); }

.vv-briefing .vv-brief-msg {
  font-family: var(--vv-font-display);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--vv-text-primary);
}

.vv-briefing.toast {
  position: fixed;
  bottom: var(--vv-space-roomy);
  right: var(--vv-space-roomy);
  z-index: 9000;
}

.vv-briefing.fading {
  animation: vvBriefingFade var(--vv-dur-cinematic) var(--vv-ease-cinematic) forwards;
}

@keyframes vvBriefingFade {
  to { opacity: 0; transform: translateY(4px); }
}
