/* ── Vision AI — the character's state machine ────────────────────────────────
   Drawn by src/assistant/VisionAiFigure.jsx. EVERY selector is scoped under `.vlai`, which sits
   on the <svg> itself, so none of these generic inner names (.eye, .disc, .prop) can leak into
   the app. Placed ABOVE the reduced-motion block on purpose — but read the note down there
   first, because that guard alone is NOT enough for this one. */
.vlai {
	--vlai-navy: var(--vl-navy);
	/* THE DISC'S HIGHLIGHT IS DERIVED, NOT PINNED. There is no `--vl-navy-hi` token, and a
	   hardcoded light-theme highlight (#1e6c96) lands within a hair of dark theme's own navy
	   (#1d6690) — the gradient computes correctly, renders as a flat disc, and nothing anywhere
	   reports a fault. `color-mix` keeps it one step lighter than whatever navy is in force, so
	   the sphere reads in both themes and follows the token if it is ever retuned. */
	--vlai-navy-hi: color-mix(in srgb, var(--vl-navy) 78%, #ffffff);
	--vlai-navy-deep: #0d3349;
	--vlai-gold: #c9a84c;
}
.vlai g, .vlai circle, .vlai ellipse, .vlai rect, .vlai path, .vlai text { transform-box: fill-box; transform-origin: 50% 50%; }
.vlai .disc { fill: var(--vlai-navy); }
.vlai .fill-light { fill: #fff; }
.vlai .fill-gold { fill: var(--vlai-gold); }
.vlai .stroke-light { stroke: #fff; fill: none; stroke-linecap: round; }
.vlai .ringline { fill: none; stroke: var(--vlai-gold); stroke-linecap: round; }
.vlai .steel { fill: none; stroke: #fff; stroke-linecap: round; opacity: .92; }
.vlai .ground { opacity: .15; fill: #000; }
.vlai .leafpage { fill: #e4ebf2; }
.vlai .lens { fill: #fff; opacity: .13; }
.vlai .zzz, .vlai .spark, .vlai .aura { opacity: 0; }
.vlai .prop { opacity: 0; }
.vlai .m-sad, .vlai .m-o, .vlai .m-flat { opacity: 0; }
.vlai .brow-l { transform-origin: 0% 50%; }
.vlai .brow-r { transform-origin: 100% 50%; }

/* THE LAUNCHER FLOATS OVER THE APP AND THE APP IS WHITE. Every prop is fill:#fff, so without a
   stroke painted BEHIND the fill the wave, the thumbs-up and the shrug render nothing at all in
   the light theme — a prop read only where it happened to overlap the navy disc. The magnifier
   and the scale hangers are STROKES and take a rim instead: a stroke cannot outline a stroke. */
.vlai .prop .fill-light, .vlai .prop .leafpage, .vlai .prop .fill-gold, .vlai .spark {
	stroke: var(--vlai-navy-deep); stroke-width: .9; paint-order: stroke fill;
}
.vlai .prop .steel { filter: drop-shadow(0 0 .9px var(--vlai-navy-deep)) drop-shadow(0 0 .9px var(--vlai-navy-deep)); }
.vlai .prop .ringline { filter: drop-shadow(0 0 .8px var(--vlai-navy-deep)); }

@keyframes vlai-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.035); } }
@keyframes vlai-breathefast { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
@keyframes vlai-breatheslow { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.02); } }
@keyframes vlai-hop { 0% { transform: translateY(0) scale(1); } 30% { transform: translateY(-6px) scale(1.03, .97); } 62% { transform: translateY(0) scale(.97, 1.03); } 100% { transform: translateY(0) scale(1); } }
@keyframes vlai-blink { 0%, 90%, 100% { transform: scaleY(1); } 94% { transform: scaleY(.08); } }
@keyframes vlai-widen { 0% { transform: scaleY(1); } 100% { transform: scaleY(1.18); } }
@keyframes vlai-dash { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(.26); } }
@keyframes vlai-shut { to { transform: scaleY(.09); } }
@keyframes vlai-lean { 0%, 100% { transform: rotate(0) translateY(0); } 50% { transform: rotate(-4deg) translateY(1.5px); } }
@keyframes vlai-weigh { 0%, 100% { transform: rotate(-12deg); } 50% { transform: rotate(12deg); } }
@keyframes vlai-spin { to { transform: rotate(360deg); } }
@keyframes vlai-propin { 0% { opacity: 0; transform: scale(.25) translate(-5px, 7px); } 62% { opacity: 1; transform: scale(1.09) translate(1px, -1px); } 100% { opacity: 1; transform: none; } }
@keyframes vlai-propup { 0% { opacity: 0; transform: scale(.92) translateY(14px); } 100% { opacity: 1; transform: none; } }
@keyframes vlai-wave { 0%, 100% { transform: rotate(0); } 15% { transform: rotate(-28deg); } 40% { transform: rotate(13deg); } 65% { transform: rotate(-22deg); } 85% { transform: rotate(7deg); } }
@keyframes vlai-chintap { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2.2px); } }
@keyframes vlai-thumbpump { 0% { transform: translateY(0); } 40% { transform: translateY(-3.5px); } 100% { transform: translateY(0); } }
@keyframes vlai-scan { 0%, 100% { transform: translate(0, 0); } 25% { transform: translate(-3px, 2px); } 75% { transform: translate(3px, -2px); } }
@keyframes vlai-pageflip { 0%, 100% { transform: rotate(0); } 50% { transform: rotate(-7deg); } }
@keyframes vlai-leafturn { 0% { transform: scaleX(1); opacity: 0; } 7% { opacity: 1; } 50% { transform: scaleX(.04); opacity: 1; } 90% { transform: scaleX(-1); opacity: 1; } 96%, 100% { transform: scaleX(-1); opacity: 0; } }
@keyframes vlai-readscan { 0% { transform: translate(-2px, 2.2px); } 70% { transform: translate(2.2px, 2.2px); } 78% { transform: translate(-2px, 2.6px); } 100% { transform: translate(-2px, 2.2px); } }
@keyframes vlai-bookbob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1.5px); } }
@keyframes vlai-browUp { to { transform: translateY(-1.6px); } }
@keyframes vlai-browFocus { to { transform: rotate(9deg); } }
@keyframes vlai-browFocusR { to { transform: rotate(-9deg); } }
@keyframes vlai-browSad { to { transform: rotate(-15deg) translateY(1px); } }
@keyframes vlai-browSadR { to { transform: rotate(15deg) translateY(1px); } }
@keyframes vlai-browDown { to { transform: translateY(1.8px); } }
@keyframes vlai-aimUp { to { transform: translate(0, -2.2px); } }
@keyframes vlai-aimUpR { to { transform: translate(2.4px, -2px); } }
@keyframes vlai-aimR { to { transform: translate(2.8px, -.8px); } }
@keyframes vlai-aimDownL { to { transform: translate(-2.6px, 2.2px); } }
@keyframes vlai-sparkpop { 0% { opacity: 0; transform: scale(.4); } 22% { opacity: 1; transform: scale(1.15); } 55% { opacity: .9; transform: scale(1); } 100% { opacity: 0; transform: scale(.5); } }
@keyframes vlai-aurapulse { 0% { opacity: .5; transform: scale(1); } 100% { opacity: 0; transform: scale(1.22); } }
@keyframes vlai-zfloat { 0% { opacity: 0; transform: translate(0, 0) scale(.7); } 25% { opacity: .95; } 100% { opacity: 0; transform: translate(4px, -11px) scale(1.15); } }
@keyframes vlai-groundpulse { 0%, 100% { transform: scaleX(1); opacity: .15; } 50% { transform: scaleX(.9); opacity: .1; } }
@keyframes vlai-talkmouth { 0%, 100% { opacity: 0; transform: scaleY(.35); } 50% { opacity: 1; transform: scaleY(1); } }

.vlai .orb { animation: vlai-breathe 4.6s ease-in-out infinite; }
.vlai .eye { animation: vlai-blink 5s ease-in-out infinite; animation-delay: var(--vlai-bd, 0s); }
.vlai .halo { animation: vlai-spin 26s linear infinite; }
.vlai .ground { animation: vlai-groundpulse 4.6s ease-in-out infinite; }

.vlai[data-state="greet"] .p-wave { opacity: 1; animation: vlai-propin .34s cubic-bezier(.34,1.56,.64,1) forwards, vlai-wave 1.15s ease-in-out .34s infinite; }
.vlai[data-state="greet"] .orb { animation: vlai-hop .6s ease-out; }
.vlai[data-state="greet"] .eye { animation: vlai-widen .2s ease-out forwards; }
.vlai[data-state="greet"] .brow-l, .vlai[data-state="greet"] .brow-r { animation: vlai-browUp .2s ease-out forwards; }

/* HOVER IS A LEAN AND A LOOK, NOT THE WAVE. The wave is worth seeing the first time the panel
   opens; on every cursor pass it wears out by lunchtime. */
.vlai[data-state="hover"] .orb { animation: vlai-lean 2.4s ease-in-out infinite; }
.vlai[data-state="hover"] .eye { animation: vlai-widen .18s ease-out forwards; }
.vlai[data-state="hover"] .brow-l, .vlai[data-state="hover"] .brow-r { animation: vlai-browUp .18s ease-out forwards; }

.vlai[data-state="listening"] .orb { animation: vlai-lean 2.6s ease-in-out infinite; }
.vlai[data-state="listening"] .eyes { animation: vlai-aimDownL 1.4s ease-in-out forwards; }
.vlai[data-state="listening"] .eye { animation: vlai-blink 3.4s ease-in-out infinite; }
.vlai[data-state="listening"] .brow-l, .vlai[data-state="listening"] .brow-r { animation: vlai-browUp .3s ease-out forwards; }

.vlai[data-state="thinking"] .p-chin { opacity: 1; animation: vlai-propin .34s cubic-bezier(.34,1.56,.64,1) forwards, vlai-chintap 1.05s ease-in-out .34s infinite; }
.vlai[data-state="thinking"] .m-smile { opacity: 0; }
.vlai[data-state="thinking"] .m-flat { opacity: 1; }
.vlai[data-state="thinking"] .orb { animation: vlai-breathefast 1.15s ease-in-out infinite; }
.vlai[data-state="thinking"] .eye { animation: vlai-dash 1.15s ease-in-out infinite; }
.vlai[data-state="thinking"] .eyes { animation: vlai-aimUp 1s ease-in-out forwards; }
.vlai[data-state="thinking"] .brow-l { animation: vlai-browFocus .3s ease-out forwards; }
.vlai[data-state="thinking"] .brow-r { animation: vlai-browFocusR .3s ease-out forwards; }
.vlai[data-state="thinking"] .aura { animation: vlai-aurapulse 1.4s ease-out infinite; }

/* the halo is the one element still legible at 22px, so it is what carries "busy" */
.vlai[data-state="thinking"] .halo, .vlai[data-state="weighing"] .halo { animation: vlai-spin 3.2s linear infinite; }
.vlai[data-state="reading"] .halo, .vlai[data-state="files"] .halo, .vlai[data-state="searching"] .halo { animation: vlai-spin 5s linear infinite; }
.vlai[data-state="sleep"] .halo { animation: vlai-spin 60s linear infinite; }

.vlai[data-state="reading"] .p-book { opacity: 1; animation: vlai-propup .4s cubic-bezier(.34,1.4,.64,1) forwards, vlai-bookbob 3s ease-in-out .4s infinite; }
.vlai[data-state="reading"] .m-smile { opacity: 0; }
.vlai[data-state="reading"] .leaf { animation: vlai-leafturn 2.6s ease-in-out .5s infinite; }
.vlai[data-state="reading"] .eyes { animation: vlai-readscan 2.6s steps(22, end) .4s infinite; }
.vlai[data-state="reading"] .eye { animation: vlai-blink 4.4s ease-in-out infinite; }
.vlai[data-state="reading"] .brow-l { animation: vlai-browFocus .3s ease-out forwards; }
.vlai[data-state="reading"] .brow-r { animation: vlai-browFocusR .3s ease-out forwards; }

.vlai[data-state="files"] .p-file { opacity: 1; animation: vlai-propin .34s cubic-bezier(.34,1.56,.64,1) forwards; }
.vlai[data-state="files"] .m-smile { opacity: 0; }
.vlai[data-state="files"] .m-flat { opacity: 1; }
.vlai[data-state="files"] .eyes { animation: vlai-aimDownL .9s ease-in-out forwards; }
.vlai[data-state="files"] .page { animation: vlai-pageflip 1.6s ease-in-out .34s infinite; }

.vlai[data-state="searching"] .p-glass { opacity: 1; animation: vlai-propin .34s cubic-bezier(.34,1.56,.64,1) forwards, vlai-scan 2.2s ease-in-out .34s infinite; }
.vlai[data-state="searching"] .m-smile { opacity: 0; }
.vlai[data-state="searching"] .m-flat { opacity: 1; }
.vlai[data-state="searching"] .eyes { animation: vlai-aimR .9s ease-in-out forwards; }
.vlai[data-state="searching"] .brow-l { animation: vlai-browUp .3s ease-out forwards; }

.vlai[data-state="weighing"] .p-scales { opacity: 1; animation: vlai-propin .38s cubic-bezier(.34,1.56,.64,1) forwards; }
.vlai[data-state="weighing"] .m-smile { opacity: 0; }
.vlai[data-state="weighing"] .m-flat { opacity: 1; }
.vlai[data-state="weighing"] .orb { animation: vlai-breathefast 1.4s ease-in-out infinite; }
.vlai[data-state="weighing"] .eyes { animation: vlai-aimUpR 1s ease-in-out forwards; }
.vlai[data-state="weighing"] .brow-l { animation: vlai-browFocus .3s ease-out forwards; }
.vlai[data-state="weighing"] .heldbeam { animation: vlai-weigh 1.3s ease-in-out .38s infinite; }

/* STREAMING — the longest stretch of a real turn had no state at all: the fist stayed up while
   the answer was visibly being written, so "thinking" and "answering" looked identical. */
.vlai[data-state="streaming"] .m-smile { opacity: 0; }
.vlai[data-state="streaming"] .m-flat { opacity: 1; }
.vlai[data-state="streaming"] .m-o { animation: vlai-talkmouth .44s ease-in-out infinite; }
.vlai[data-state="streaming"] .halo { animation: vlai-spin 8s linear infinite; }
.vlai[data-state="streaming"] .orb { animation: vlai-breathe 3.2s ease-in-out infinite; }
.vlai[data-state="streaming"] .eye { animation: vlai-blink 4s ease-in-out infinite; }

.vlai[data-state="answer"] .p-thumb { opacity: 1; animation: vlai-propin .36s cubic-bezier(.34,1.56,.64,1) forwards, vlai-thumbpump .5s ease-out .36s; }
.vlai[data-state="answer"] .m-smile { opacity: 0; }
.vlai[data-state="answer"] .m-o { opacity: 1; }
.vlai[data-state="answer"] .orb { animation: vlai-hop .65s ease-out; }
.vlai[data-state="answer"] .eye { animation: vlai-widen .25s ease-out forwards; }
.vlai[data-state="answer"] .brow-l, .vlai[data-state="answer"] .brow-r { animation: vlai-browUp .25s ease-out forwards; }
.vlai[data-state="answer"] .spark { animation: vlai-sparkpop 2s ease-out infinite; }
.vlai[data-state="answer"] .s2 { animation-delay: .18s; }
.vlai[data-state="answer"] .s3 { animation-delay: .36s; }
.vlai[data-state="answer"] .s4 { animation-delay: .54s; }

.vlai[data-state="empty"] .p-shrugl, .vlai[data-state="empty"] .p-shrugr { opacity: 1; animation: vlai-propin .32s cubic-bezier(.34,1.56,.64,1) forwards; }
.vlai[data-state="empty"] .m-smile { opacity: 0; }
.vlai[data-state="empty"] .m-sad { opacity: 1; }
.vlai[data-state="empty"] .orb { animation: vlai-breatheslow 4s ease-in-out infinite; }
.vlai[data-state="empty"] .eye { animation: vlai-dash .01s linear forwards; }
.vlai[data-state="empty"] .brow-l { animation: vlai-browSad .3s ease-out forwards; }
.vlai[data-state="empty"] .brow-r { animation: vlai-browSadR .3s ease-out forwards; }

.vlai[data-state="sleep"] .orb { animation: vlai-breatheslow 7s ease-in-out infinite; }
.vlai[data-state="sleep"] .eye { animation: vlai-shut .3s ease-out forwards; }
.vlai[data-state="sleep"] .brow-l, .vlai[data-state="sleep"] .brow-r { animation: vlai-browDown .4s ease-out forwards; }
.vlai[data-state="sleep"] .zzz { animation: vlai-zfloat 2.6s ease-out infinite; }
.vlai[data-state="sleep"] .z2 { animation-delay: .9s; }

/* ── Thrown. The character goes WITH the throw: eyes wide in flight, a squint on the hit, and it
   shakes it off on landing. Without the face the orb is a ball, and the whole reason to build this
   was that it is not one. */
@keyframes vlai-shakeoff { 0% { transform: rotate(0); } 18% { transform: rotate(8deg); }
  42% { transform: rotate(-7deg); } 68% { transform: rotate(3.5deg); } 100% { transform: rotate(0); } }
@keyframes vlai-squint { 0% { transform: scaleY(1); } 45% { transform: scaleY(.2); } 100% { transform: scaleY(1); } }

.vlai[data-state="grab"] .m-smile, .vlai[data-state="flight"] .m-smile { opacity: 0; }
.vlai[data-state="grab"] .m-o, .vlai[data-state="flight"] .m-o { opacity: 1; }
.vlai[data-state="grab"] .eye { animation: vlai-widen .14s ease-out forwards; }
.vlai[data-state="flight"] .eye { animation: vlai-widen .1s ease-out forwards; }
.vlai[data-state="grab"] .brow-l, .vlai[data-state="grab"] .brow-r,
.vlai[data-state="flight"] .brow-l, .vlai[data-state="flight"] .brow-r { animation: vlai-browUp .12s ease-out forwards; }
.vlai[data-state="grab"] .orb, .vlai[data-state="flight"] .orb,
.vlai[data-state="impact"] .orb { animation: none; }
.vlai[data-state="flight"] .halo { animation: vlai-spin 1.1s linear infinite; }
.vlai[data-state="impact"] .m-smile { opacity: 0; }
.vlai[data-state="impact"] .m-flat { opacity: 1; }
.vlai[data-state="impact"] .eye { animation: vlai-squint .22s ease-out; }
.vlai[data-state="impact"] .brow-l { animation: vlai-browFocus .1s ease-out forwards; }
.vlai[data-state="impact"] .brow-r { animation: vlai-browFocusR .1s ease-out forwards; }
.vlai[data-state="land"] .m-smile { opacity: 0; }
.vlai[data-state="land"] .m-flat { opacity: 1; }
.vlai[data-state="land"] .orb { animation: vlai-shakeoff .52s ease-out; }
.vlai[data-state="land"] .eye { animation: vlai-blink .6s ease-in-out; }
/* the ground shadow must NOT fly with it — a shadow travelling with a thrown object is the tell
   that nothing is really moving */
.vlai[data-state="grab"] .ground, .vlai[data-state="flight"] .ground,
.vlai[data-state="impact"] .ground { opacity: 0; }

/* DOZING — docked and untouched, it fades back and stops moving; the pointer coming near brings
   it out. It recedes without ever HIDING: a control that removes itself is a support call. */
.vl-asst-orb { transition: opacity .5s ease; }
.vl-asst-doze { opacity: .55; }
.vl-asst-doze .vlai * { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
	*, *::before, *::after {
		animation-duration: 0.01ms !important;
		animation-delay: 0ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
	}

	/* AN EXPRESSION CARRIED ONLY BY AN ANIMATION'S `forwards` FILL DISAPPEARS when animations are
	   switched off, and the blanket rule above does exactly that. Four of the character's states
	   silently lost their brows and eyes and fell back to the RESTING FACE while the mouth still
	   changed — a half-working face, which reads as a design choice rather than as a fault and is
	   therefore worse than a still one. Every meaningful end state is declared STATICALLY here as
	   well; the animation only ever carries the transition. Verified by killing animations and
	   reading the computed transform on each state's brows and eyes.

	   THE STATICS MUST BE UNOPPOSED, which is why the character's animations are switched OFF here
	   rather than merely shortened. The blanket rule above only clamps DURATION to 0.01ms — the
	   animations still run, and an animation in effect beats a normal declaration, so the resting
	   `blink` keyframe (`transform: scaleY(1)`) wins over a static `scaleY(.26)` and the face
	   silently falls back to neutral. Shortening a loop is not the same as removing it. */
	.vlai *, .vlai *::before, .vlai *::after { animation: none !important; }

	.vlai[data-state="greet"] .eye, .vlai[data-state="hover"] .eye, .vlai[data-state="answer"] .eye { transform: scaleY(1.18); }
	.vlai[data-state="sleep"] .eye { transform: scaleY(.09); }
	.vlai[data-state="empty"] .eye, .vlai[data-state="thinking"] .eye { transform: scaleY(.26); }
	.vlai[data-state="greet"] .brow-l, .vlai[data-state="greet"] .brow-r,
	.vlai[data-state="hover"] .brow-l, .vlai[data-state="hover"] .brow-r,
	.vlai[data-state="answer"] .brow-l, .vlai[data-state="answer"] .brow-r,
	.vlai[data-state="listening"] .brow-l, .vlai[data-state="listening"] .brow-r,
	.vlai[data-state="searching"] .brow-l { transform: translateY(-1.6px); }
	.vlai[data-state="thinking"] .brow-l, .vlai[data-state="reading"] .brow-l,
	.vlai[data-state="weighing"] .brow-l { transform: rotate(9deg); }
	.vlai[data-state="thinking"] .brow-r, .vlai[data-state="reading"] .brow-r { transform: rotate(-9deg); }
	.vlai[data-state="empty"] .brow-l { transform: rotate(-15deg) translateY(1px); }
	.vlai[data-state="empty"] .brow-r { transform: rotate(15deg) translateY(1px); }
	.vlai[data-state="sleep"] .brow-l, .vlai[data-state="sleep"] .brow-r { transform: translateY(1.8px); }
	.vlai[data-state="listening"] .eyes, .vlai[data-state="files"] .eyes { transform: translate(-2.6px, 2.2px); }
	.vlai[data-state="thinking"] .eyes { transform: translate(0, -2.2px); }
	.vlai[data-state="searching"] .eyes { transform: translate(2.8px, -.8px); }
	.vlai[data-state="weighing"] .eyes { transform: translate(2.4px, -2px); }
	.vlai[data-state="streaming"] .m-o { opacity: 1; }
	/* the throw resolves straight to the docked edge under this guard (useThrowable checks the
	   media query itself), so `flight` and `impact` never appear — but `grab` and `land` still do,
	   because DRAGGING is direct manipulation and stays. */
	.vlai[data-state="grab"] .eye { transform: scaleY(1.18); }
	.vlai[data-state="grab"] .brow-l, .vlai[data-state="grab"] .brow-r { transform: translateY(-1.6px); }
	/* the props themselves survive: every state above sets `opacity: 1` in its own declaration,
	   never only through the propin animation's forwards fill. */
}

/* ── Focus visibility ────────────────────────────────────────────────────────
   Nothing in the app showed a focus ring, so a keyboard user could not tell where they were.
   :focus-visible only paints for keyboard/AT focus, so mouse users see no change. */
:focus-visible {
	outline: 2px solid var(--vl-navy);
	outline-offset: 2px;
	border-radius: 4px;
}
/* inputs already draw their own navy border on focus — avoid doubling it up */
input:focus-visible, textarea:focus-visible, select:focus-visible { outline-offset: 0; }

@media print {
	/* Chrome (sidebar, topbar, toasts, dialogs, floating bars) is noise on paper; the page content
	   should own the sheet. Record screens have their own PDF exports — this is for a quick Ctrl+P. */
	aside, .vl-loadbar, .vl-toast, [data-print="hide"] { display: none !important; }
	main { overflow: visible !important; padding: 0 !important; }
	body { background: #fff !important; }
	* { box-shadow: none !important; }
	a[href]::after { content: ''; } /* don't append URLs after links */
	table { break-inside: auto; }
	tr, .vl-row { break-inside: avoid; }
}
