/* =============================================================================
   TRINITY ANIMATION VOCABULARY
   One keyframe library shared by Remandly, RetainerFund and FamilyLawPath.

   Every animation here is declared as a @keyframes block and exposed through a
   utility class, so a surface can be animated from markup without new script.
   GSAP and Motion One drive the sequenced work; these carry the ambient and
   state-based motion, and they are the floor the page falls back to when both
   engines fail to load.

   Rules that hold throughout:
     - Nothing loops forever except deliberate ambient texture.
     - Nothing bounces on a money surface.
     - prefers-reduced-motion kills all of it at the bottom of the file.
   ============================================================================= */

/* ----------------------------------------------------------------------------
   1. ENTRANCES
   ---------------------------------------------------------------------------- */
@keyframes tFadeIn            { from { opacity: 0 } to { opacity: 1 } }
@keyframes tRiseIn            { from { opacity: 0; transform: translateY(18px) } to { opacity: 1; transform: translateY(0) } }
@keyframes tRiseInSm          { from { opacity: 0; transform: translateY(8px) }  to { opacity: 1; transform: translateY(0) } }
@keyframes tRiseInLg          { from { opacity: 0; transform: translateY(40px) } to { opacity: 1; transform: translateY(0) } }
@keyframes tDropIn            { from { opacity: 0; transform: translateY(-16px) } to { opacity: 1; transform: translateY(0) } }
@keyframes tSlideInLeft       { from { opacity: 0; transform: translateX(-24px) } to { opacity: 1; transform: translateX(0) } }
@keyframes tSlideInRight      { from { opacity: 0; transform: translateX(24px) }  to { opacity: 1; transform: translateX(0) } }
@keyframes tScaleIn           { from { opacity: 0; transform: scale(0.96) } to { opacity: 1; transform: scale(1) } }
@keyframes tScaleInSoft       { from { opacity: 0; transform: scale(0.985) } to { opacity: 1; transform: scale(1) } }
@keyframes tUnfoldDown        { from { opacity: 0; transform: scaleY(0.7); transform-origin: top } to { opacity: 1; transform: scaleY(1); transform-origin: top } }
@keyframes tUnfoldUp          { from { opacity: 0; transform: scaleY(0.7); transform-origin: bottom } to { opacity: 1; transform: scaleY(1); transform-origin: bottom } }
@keyframes tWipeInX           { from { clip-path: inset(0 100% 0 0) } to { clip-path: inset(0 0 0 0) } }
@keyframes tWipeInY           { from { clip-path: inset(100% 0 0 0) } to { clip-path: inset(0 0 0 0) } }
@keyframes tIrisIn            { from { clip-path: circle(0% at 50% 50%) } to { clip-path: circle(140% at 50% 50%) } }
@keyframes tBlurIn            { from { opacity: 0; filter: blur(8px) } to { opacity: 1; filter: blur(0) } }

/* ----------------------------------------------------------------------------
   2. EXITS
   ---------------------------------------------------------------------------- */
@keyframes tFadeOut           { from { opacity: 1 } to { opacity: 0 } }
@keyframes tSinkOut           { from { opacity: 1; transform: translateY(0) } to { opacity: 0; transform: translateY(12px) } }
@keyframes tLiftOut           { from { opacity: 1; transform: translateY(0) } to { opacity: 0; transform: translateY(-12px) } }
@keyframes tScaleOut          { from { opacity: 1; transform: scale(1) } to { opacity: 0; transform: scale(0.97) } }
@keyframes tWipeOutX          { from { clip-path: inset(0 0 0 0) } to { clip-path: inset(0 0 0 100%) } }
@keyframes tCollapseUp        { from { opacity: 1; max-height: 400px } to { opacity: 0; max-height: 0 } }
@keyframes tBlurOut           { from { opacity: 1; filter: blur(0) } to { opacity: 0; filter: blur(6px) } }
@keyframes tDismissRight      { from { opacity: 1; transform: translateX(0) } to { opacity: 0; transform: translateX(32px) } }

/* ----------------------------------------------------------------------------
   3. EMPHASIS
   Used sparingly. A pulse on a money figure is a claim, so it fires once.
   ---------------------------------------------------------------------------- */
@keyframes tPulse             { 0%,100% { transform: scale(1) } 50% { transform: scale(1.035) } }
@keyframes tPulseSoft         { 0%,100% { transform: scale(1) } 50% { transform: scale(1.012) } }
@keyframes tNudgeUp           { 0%,100% { transform: translateY(0) } 40% { transform: translateY(-4px) } }
@keyframes tNudgeRight        { 0%,100% { transform: translateX(0) } 40% { transform: translateX(4px) } }
@keyframes tShakeX            { 0%,100% { transform: translateX(0) } 20% { transform: translateX(-5px) } 40% { transform: translateX(5px) } 60% { transform: translateX(-3px) } 80% { transform: translateX(3px) } }
@keyframes tAttentionRing     { from { box-shadow: 0 0 0 0 rgba(99,91,255,0.35) } to { box-shadow: 0 0 0 14px rgba(99,91,255,0) } }
@keyframes tAttentionRingWarm { from { box-shadow: 0 0 0 0 rgba(255,89,0,0.35) } to { box-shadow: 0 0 0 14px rgba(255,89,0,0) } }
@keyframes tAttentionRingGreen{ from { box-shadow: 0 0 0 0 rgba(16,185,129,0.35) } to { box-shadow: 0 0 0 14px rgba(16,185,129,0) } }
@keyframes tUnderlineGrow     { from { transform: scaleX(0); transform-origin: left } to { transform: scaleX(1); transform-origin: left } }
@keyframes tHighlightSweep    { from { background-position: -200% 0 } to { background-position: 200% 0 } }
@keyframes tBorderWake        { 0% { border-color: var(--hairline) } 50% { border-color: var(--accent) } 100% { border-color: var(--hairline) } }
@keyframes tTextSettle        { from { letter-spacing: 0.08em; opacity: 0 } to { letter-spacing: 0; opacity: 1 } }

/* ----------------------------------------------------------------------------
   4. DOCUMENT CAMERA
   The capture surface has its own vocabulary because nothing else in the
   product asks somebody to hold a phone steady over a court document.
   ---------------------------------------------------------------------------- */
@keyframes tGuideBreathe      { 0%,100% { opacity: 0.55 } 50% { opacity: 0.85 } }
@keyframes tGuideLock         { from { opacity: 0.85; transform: scale(1) } to { opacity: 0; transform: scale(1.03) } }
@keyframes tEdgeTrace         { from { stroke-dashoffset: 400 } to { stroke-dashoffset: 0 } }
@keyframes tEdgePulse         { 0%,100% { stroke-opacity: 0.65 } 50% { stroke-opacity: 1 } }
@keyframes tEdgeLockFlash     { 0% { stroke-width: 2 } 45% { stroke-width: 4 } 100% { stroke-width: 2 } }
@keyframes tCornerPop         { from { transform: scale(0.4); opacity: 0 } to { transform: scale(1); opacity: 1 } }
@keyframes tScanSweep         { from { transform: translateY(-100%) } to { transform: translateY(1100%) } }
@keyframes tShutterFlash      { 0% { opacity: 0 } 12% { opacity: 0.9 } 100% { opacity: 0 } }
@keyframes tFreezeFrame       { from { filter: saturate(1) } to { filter: saturate(0.35) } }
@keyframes tWarpSettle        { from { transform: perspective(900px) rotateX(6deg) scale(0.95); opacity: 0 } to { transform: perspective(900px) rotateX(0) scale(1); opacity: 1 } }
@keyframes tBlurWarn          { 0%,100% { opacity: 0.85 } 50% { opacity: 1 } }
@keyframes tRetakeSlideIn     { from { opacity: 0; transform: translateY(20px) } to { opacity: 1; transform: translateY(0) } }
@keyframes tFieldReveal       { from { opacity: 0; transform: translateX(-10px) } to { opacity: 1; transform: translateX(0) } }
@keyframes tFieldUnderline    { from { width: 0 } to { width: 100% } }
@keyframes tParsedCardRise    { from { opacity: 0; transform: translateY(28px) scale(0.97) } to { opacity: 1; transform: translateY(0) scale(1) } }
@keyframes tChipDock          { from { transform: translateY(-14px) scale(0.9); opacity: 0 } to { transform: translateY(0) scale(1); opacity: 1 } }
@keyframes tTorchGlow         { 0%,100% { opacity: 0.4 } 50% { opacity: 0.9 } }

/* ----------------------------------------------------------------------------
   5. PATH AND RAIL
   Stage illumination for FamilyLawPath, freight movement for RetainerFund.
   ---------------------------------------------------------------------------- */
@keyframes tPathSpark         { from { height: 0; opacity: 1 } to { height: var(--spark-end, 100%); opacity: 1 } }
@keyframes tNodeWake          { from { transform: scale(0.85); opacity: 0.4 } to { transform: scale(1); opacity: 1 } }
@keyframes tNodeHalo          { from { box-shadow: 0 0 0 0 rgba(16,185,129,0.4) } to { box-shadow: 0 0 0 12px rgba(16,185,129,0) } }
@keyframes tNodeDotFill       { from { transform: scale(0) } to { transform: scale(1) } }
@keyframes tConnectorDraw     { from { transform: scaleY(0); transform-origin: top } to { transform: scaleY(1); transform-origin: top } }
@keyframes tRailFill          { from { width: 0 } to { width: 100% } }
@keyframes tRailFreight       { from { left: 0; opacity: 0 } 10% { opacity: 1 } 90% { opacity: 1 } to { left: 100%; opacity: 0 } }
@keyframes tStationLight      { from { border-color: var(--hairline); background: transparent } to { border-color: var(--accent); background: rgba(99,91,255,0.12) } }
@keyframes tStationTerminal   { from { background: transparent } to { background: #fff } }
@keyframes tTrailGlow         { 0%,100% { opacity: 0.5 } 50% { opacity: 1 } }

/* ----------------------------------------------------------------------------
   6. DATA AND NUMBERS
   ---------------------------------------------------------------------------- */
@keyframes tDigitRoll         { from { transform: translateY(0.5em); opacity: 0 } to { transform: translateY(0); opacity: 1 } }
@keyframes tRingSweep         { from { stroke-dashoffset: 364.4 } to { stroke-dashoffset: 0 } }
@keyframes tRingSettle        { 0% { transform: rotate(-90deg) scale(0.97) } 100% { transform: rotate(-90deg) scale(1) } }
@keyframes tBarGrow           { from { transform: scaleY(0); transform-origin: bottom } to { transform: scaleY(1); transform-origin: bottom } }
@keyframes tBarGrowX          { from { transform: scaleX(0); transform-origin: left } to { transform: scaleX(1); transform-origin: left } }
@keyframes tSparkLine         { from { stroke-dashoffset: 300 } to { stroke-dashoffset: 0 } }
@keyframes tRowFlashCool      { 0% { background: rgba(99,91,255,0) } 45% { background: rgba(99,91,255,0.16) } 100% { background: rgba(99,91,255,0) } }
@keyframes tRowFlashWarm      { 0% { background: rgba(255,89,0,0) } 45% { background: rgba(255,89,0,0.14) } 100% { background: rgba(255,89,0,0) } }
@keyframes tRowFlashGreen     { 0% { background: rgba(16,185,129,0) } 45% { background: rgba(16,185,129,0.14) } 100% { background: rgba(16,185,129,0) } }
@keyframes tTickIn            { from { transform: scale(0) rotate(-20deg); opacity: 0 } to { transform: scale(1) rotate(0); opacity: 1 } }
@keyframes tCheckDraw         { from { stroke-dashoffset: 32 } to { stroke-dashoffset: 0 } }
@keyframes tCountUpFade       { from { opacity: 0.35 } to { opacity: 1 } }

/* ----------------------------------------------------------------------------
   7. LOADING — skeletons, never spinners
   ---------------------------------------------------------------------------- */
@keyframes tSkeleton          { from { background-position: -200% 0 } to { background-position: 200% 0 } }
@keyframes tSkeletonPulse     { 0%,100% { opacity: 0.55 } 50% { opacity: 0.9 } }
@keyframes tProgressIndet     { from { transform: translateX(-100%) } to { transform: translateX(400%) } }
@keyframes tDotStagger1       { 0%,80%,100% { opacity: 0.25 } 40% { opacity: 1 } }
@keyframes tDotStagger2       { 0%,80%,100% { opacity: 0.25 } 50% { opacity: 1 } }
@keyframes tDotStagger3       { 0%,80%,100% { opacity: 0.25 } 60% { opacity: 1 } }
@keyframes tTypeCaret         { 0%,49% { opacity: 1 } 50%,100% { opacity: 0 } }
@keyframes tShimmerSoft       { from { background-position: -120% 0 } to { background-position: 220% 0 } }

/* ----------------------------------------------------------------------------
   8. MICRO-INTERACTION
   ---------------------------------------------------------------------------- */
@keyframes tChipTap           { 0% { transform: scale(1) } 40% { transform: scale(0.95) } 100% { transform: scale(1) } }
@keyframes tButtonPress       { 0% { transform: scale(1) } 45% { transform: scale(0.975) } 100% { transform: scale(1) } }
@keyframes tToggleSlide       { from { transform: translateX(0) } to { transform: translateX(18px) } }
@keyframes tFocusRing         { from { box-shadow: 0 0 0 0 rgba(255,255,255,0.28) } to { box-shadow: 0 0 0 5px rgba(255,255,255,0) } }
@keyframes tTooltipIn         { from { opacity: 0; transform: translateY(4px) scale(0.97) } to { opacity: 1; transform: translateY(0) scale(1) } }
@keyframes tToastIn           { from { opacity: 0; transform: translateY(24px) } to { opacity: 1; transform: translateY(0) } }
@keyframes tToastOut          { from { opacity: 1; transform: translateY(0) } to { opacity: 0; transform: translateY(16px) } }
@keyframes tUndoDrain         { from { transform: scaleX(1); transform-origin: left } to { transform: scaleX(0); transform-origin: left } }
@keyframes tSheetUp           { from { transform: translateY(100%) } to { transform: translateY(0) } }
@keyframes tSheetDown         { from { transform: translateY(0) } to { transform: translateY(100%) } }
@keyframes tBackdropIn        { from { opacity: 0 } to { opacity: 1 } }
@keyframes tCaretBlink        { 0%,49% { opacity: 1 } 50%,100% { opacity: 0 } }

/* ----------------------------------------------------------------------------
   9. FLIP AND INVERSION — the two set pieces
   ---------------------------------------------------------------------------- */
@keyframes tCardFlip          { from { transform: perspective(1200px) rotateY(0) } to { transform: perspective(1200px) rotateY(180deg) } }
@keyframes tCardFlipBack      { from { transform: perspective(1200px) rotateY(180deg) } to { transform: perspective(1200px) rotateY(360deg) } }
@keyframes tCanvasInvert      { from { clip-path: circle(0% at 50% 55%) } to { clip-path: circle(150% at 50% 55%) } }
@keyframes tCanvasSettle      { from { opacity: 1 } to { opacity: 0 } }
@keyframes tSurfaceSwap       { 0% { filter: none } 50% { filter: brightness(1.06) } 100% { filter: none } }

/* ----------------------------------------------------------------------------
   10. SAFETY DETOUR
   Deliberately flat. Nothing here rewards, celebrates, or draws the eye. If a
   person is in danger the interface gets quieter, not louder.
   ---------------------------------------------------------------------------- */
@keyframes tSafetyFade        { from { opacity: 0 } to { opacity: 1 } }
@keyframes tSafetyCalm        { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: translateY(0) } }
@keyframes tExitFlash         { 0% { opacity: 1 } 100% { opacity: 0 } }
@keyframes tQuietDim          { from { opacity: 1 } to { opacity: 0.55 } }

/* ----------------------------------------------------------------------------
   11. AMBIENT TEXTURE
   The only looping animations in the system. Slow, low contrast, background.
   ---------------------------------------------------------------------------- */
@keyframes tDriftSlow         { 0%,100% { transform: translate3d(0,0,0) } 50% { transform: translate3d(0,-14px,0) } }
@keyframes tDriftSlower       { 0%,100% { transform: translate3d(0,0,0) } 50% { transform: translate3d(10px,-8px,0) } }
@keyframes tGrainShift        { 0% { background-position: 0 0 } 100% { background-position: 64px 64px } }
@keyframes tAuroraPan         { 0% { background-position: 0% 50% } 50% { background-position: 100% 50% } 100% { background-position: 0% 50% } }
@keyframes tHairlineBreathe   { 0%,100% { opacity: 0.5 } 50% { opacity: 0.85 } }

/* =============================================================================
   UTILITY CLASSES
   ============================================================================= */
.a-fade        { animation: tFadeIn var(--dur-med,300ms) var(--ease) both }
.a-rise        { animation: tRiseIn var(--dur-med,300ms) var(--ease) both }
.a-rise-sm     { animation: tRiseInSm 240ms var(--ease) both }
.a-rise-lg     { animation: tRiseInLg var(--dur-slow,600ms) var(--ease) both }
.a-drop        { animation: tDropIn var(--dur-med,300ms) var(--ease) both }
.a-in-left     { animation: tSlideInLeft var(--dur-med,300ms) var(--ease) both }
.a-in-right    { animation: tSlideInRight var(--dur-med,300ms) var(--ease) both }
.a-scale       { animation: tScaleIn var(--dur-med,300ms) var(--ease) both }
.a-unfold      { animation: tUnfoldDown var(--dur-med,300ms) var(--ease) both }
.a-wipe-x      { animation: tWipeInX var(--dur-slow,600ms) var(--ease) both }
.a-wipe-y      { animation: tWipeInY var(--dur-slow,600ms) var(--ease) both }
.a-iris        { animation: tIrisIn var(--dur-hero,800ms) var(--ease) both }
.a-blur-in     { animation: tBlurIn var(--dur-slow,600ms) var(--ease) both }
.a-out         { animation: tFadeOut var(--dur-fast,150ms) var(--ease) both }
.a-sink        { animation: tSinkOut var(--dur-med,300ms) var(--ease) both }
.a-lift        { animation: tLiftOut var(--dur-med,300ms) var(--ease) both }
.a-dismiss     { animation: tDismissRight var(--dur-med,300ms) var(--ease) both }
.a-pulse       { animation: tPulse 420ms var(--ease) 1 }
.a-pulse-soft  { animation: tPulseSoft 520ms var(--ease) 1 }
.a-nudge       { animation: tNudgeUp 360ms var(--ease) 1 }
.a-shake       { animation: tShakeX 420ms var(--ease) 1 }
.a-ring        { animation: tAttentionRing 700ms var(--ease) 1 }
.a-ring-warm   { animation: tAttentionRingWarm 700ms var(--ease) 1 }
.a-ring-green  { animation: tAttentionRingGreen 700ms var(--ease) 1 }
.a-underline   { animation: tUnderlineGrow var(--dur-slow,600ms) var(--ease) both }
.a-settle      { animation: tTextSettle var(--dur-slow,600ms) var(--ease) both }
.a-guide       { animation: tGuideBreathe 2.4s ease-in-out infinite }
.a-guide-lock  { animation: tGuideLock 320ms var(--ease) both }
.a-edge-trace  { animation: tEdgeTrace var(--dur-slow,600ms) linear both }
.a-edge-pulse  { animation: tEdgePulse 1.2s ease-in-out infinite }
.a-edge-lock   { animation: tEdgeLockFlash 320ms var(--ease) 1 }
.a-corner      { animation: tCornerPop 260ms var(--ease) both }
.a-scan        { animation: tScanSweep 1.6s linear infinite }
.a-shutter     { animation: tShutterFlash 240ms linear 1 }
.a-warp        { animation: tWarpSettle 520ms var(--ease) both }
.a-retake      { animation: tRetakeSlideIn 320ms var(--ease) both }
.a-field       { animation: tFieldReveal 280ms var(--ease) both }
.a-parsed      { animation: tParsedCardRise 520ms var(--ease) both }
.a-chip-dock   { animation: tChipDock 340ms var(--ease) both }
.a-node-wake   { animation: tNodeWake 420ms var(--ease) both }
.a-node-halo   { animation: tNodeHalo 700ms var(--ease) 1 }
.a-connector   { animation: tConnectorDraw 500ms var(--ease) both }
.a-rail-fill   { animation: tRailFill 900ms var(--ease) both }
.a-freight     { animation: tRailFreight 900ms var(--ease) both }
.a-digit       { animation: tDigitRoll 260ms var(--ease) both }
.a-ring-sweep  { animation: tRingSweep var(--dur-hero,800ms) var(--ease) both }
.a-bar         { animation: tBarGrow var(--dur-slow,600ms) var(--ease) both }
.a-bar-x       { animation: tBarGrowX var(--dur-slow,600ms) var(--ease) both }
.a-row-cool    { animation: tRowFlashCool 400ms var(--ease) 1 }
.a-row-warm    { animation: tRowFlashWarm 400ms var(--ease) 1 }
.a-row-green   { animation: tRowFlashGreen 400ms var(--ease) 1 }
.a-tick        { animation: tTickIn 320ms var(--ease-spring, var(--ease)) both }
.a-check       { animation: tCheckDraw 420ms var(--ease) both }
.a-skeleton    { animation: tSkeleton 1.4s linear infinite }
.a-skeleton-p  { animation: tSkeletonPulse 1.4s ease-in-out infinite }
.a-indet       { animation: tProgressIndet 1.2s var(--ease) infinite }
.a-dot-1       { animation: tDotStagger1 1.2s linear infinite }
.a-dot-2       { animation: tDotStagger2 1.2s linear infinite }
.a-dot-3       { animation: tDotStagger3 1.2s linear infinite }
.a-caret       { animation: tTypeCaret 1s step-end infinite }
.a-chip-tap    { animation: tChipTap 220ms var(--ease) 1 }
.a-press       { animation: tButtonPress 180ms var(--ease) 1 }
.a-tooltip     { animation: tTooltipIn 180ms var(--ease) both }
.a-toast-in    { animation: tToastIn 320ms var(--ease) both }
.a-toast-out   { animation: tToastOut 240ms var(--ease) both }
.a-undo-drain  { animation: tUndoDrain 5s linear both }
.a-sheet-up    { animation: tSheetUp 320ms var(--ease) both }
.a-sheet-down  { animation: tSheetDown 260ms var(--ease) both }
.a-backdrop    { animation: tBackdropIn 200ms var(--ease) both }
.a-flip        { animation: tCardFlip var(--dur-slow,600ms) var(--ease-in-out, ease-in-out) both }
.a-invert      { animation: tCanvasInvert var(--dur-hero,800ms) var(--ease-in-out, ease-in-out) both }
.a-safety      { animation: tSafetyCalm 320ms var(--ease) both }
.a-exit-flash  { animation: tExitFlash 160ms linear both }
.a-quiet       { animation: tQuietDim 300ms var(--ease) both }
.a-drift       { animation: tDriftSlow 14s ease-in-out infinite }
.a-drift-slow  { animation: tDriftSlower 22s ease-in-out infinite }
.a-grain       { animation: tGrainShift 8s linear infinite }
.a-aurora      { animation: tAuroraPan 24s ease-in-out infinite }
.a-hairline    { animation: tHairlineBreathe 6s ease-in-out infinite }

/* Stagger helpers, so a group can be delayed from markup alone. */
.a-d1 { animation-delay: 60ms }
.a-d2 { animation-delay: 120ms }
.a-d3 { animation-delay: 180ms }
.a-d4 { animation-delay: 240ms }
.a-d5 { animation-delay: 300ms }
.a-d6 { animation-delay: 360ms }

/* The wash element GSAP appends for the canvas inversion. */
.gsap-wash {
  position: fixed; inset: 0; z-index: 30; pointer-events: none;
  background: var(--canvas, #08090A);
}

/* =============================================================================
   REDUCED MOTION
   One switch, and it is not negotiable. Ambient loops stop, transitions become
   instant, and every animated element lands in its finished state.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
  [class*="a-"] {
    animation: none !important;
    transition: none !important;
  }
  .gsap-wash { display: none !important; }
}


/* =============================================================================
   ASSET-BACKED SURFACES
   The SVGs in assets/ are load-bearing, not decoration sitting in a folder.
   ============================================================================= */

/* Hairline grid behind the hero. Very low contrast; it reads as paper texture
   rather than as a graph, which is the point. */
.hero::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background-image: url("assets/grid.svg");
  background-size: 64px 64px;
  opacity: 0.22;
  mask-image: linear-gradient(180deg, rgba(0,0,0,0.9), transparent 78%);
  -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,0.9), transparent 78%);
}
.hero { position: relative; }
.hero > * { position: relative; z-index: 1; }

/* Film grain over dark surfaces. Kills the flat digital gradient banding that
   makes a dark page look cheap on a projector. */
.grain-layer {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background-image: url("assets/noise.svg");
  background-size: 180px 180px;
  opacity: 0.045;
  mix-blend-mode: overlay;
}

/* The document guide frame inside the camera view is the authored SVG, so the
   corner brackets and the dashed edge stay identical between platforms. */
.cap-guide-art {
  position: absolute; inset: 0;
  background: url("assets/doc-guide.svg") center / contain no-repeat;
  pointer-events: none;
  animation: tGuideBreathe 2.4s ease-in-out infinite;
}
.cap-guide.is-locked .cap-guide-art { animation: tGuideLock 320ms var(--ease) both; }

/* Empty states. Every table and list that can be empty gets the drawn page. */
.empty-art {
  width: 180px; height: 126px; margin: 0 auto var(--space-4, 16px);
  background: url("assets/empty-state.svg") center / contain no-repeat;
  opacity: 0.85;
}

/* Light mode carries texture much more loudly than dark. Halve it. */
[data-mode="light"] .hero::before { opacity: 0.10; }
[data-mode="light"] .grain-layer  { opacity: 0.02; mix-blend-mode: multiply; }
