loki-mode 6.35.1 → 6.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,-apple-system,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,Fira Code,Cascadia Code,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.terminal-scroll::-webkit-scrollbar{width:6px}.terminal-scroll::-webkit-scrollbar-track{background:transparent}.terminal-scroll::-webkit-scrollbar-thumb{background:#3d52a033;border-radius:3px}.terminal-scroll::-webkit-scrollbar-thumb:hover{background:#3d52a059}.glass{background:#fff9;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,.3);border-radius:16px;box-shadow:0 8px 32px #3d52a014}.glass-subtle{background:#fff6;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.2);border-radius:12px;box-shadow:0 4px 16px #3d52a00f}.pattern-circles{position:absolute;top:0;right:0;bottom:0;left:0;opacity:.04;background-image:radial-gradient(circle at 20% 30%,transparent 30%,#6C63FF 30.5%,transparent 31%),radial-gradient(circle at 80% 70%,transparent 25%,#6C63FF 25.5%,transparent 26%),radial-gradient(circle at 50% 50%,transparent 40%,#6C63FF 40.5%,transparent 41%);pointer-events:none}@keyframes phase-pulse{0%,to{opacity:1}50%{opacity:.5}}.phase-active{animation:phase-pulse 2s ease-in-out infinite}@keyframes cursor-blink{0%,to{opacity:1}50%{opacity:0}}.terminal-cursor:after{content:"";display:inline-block;width:8px;height:16px;background:#7091e6;animation:cursor-blink 1s step-end infinite;margin-left:2px;vertical-align:text-bottom}.\!visible{visibility:visible!important}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.right-0{right:0}.top-0{top:0}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-5{grid-column:span 5 / span 5}.col-span-6{grid-column:span 6 / span 6}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-28{height:7rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.max-h-32{max-height:8rem}.max-h-40{max-height:10rem}.max-h-64{max-height:16rem}.max-h-\[400px\]{max-height:400px}.min-h-0{min-height:0px}.min-h-\[280px\]{min-height:280px}.min-h-\[300px\]{min-height:300px}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-56{width:14rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.max-w-3xl{max-width:48rem}.max-w-\[1920px\]{max-width:1920px}.max-w-\[220px\]{max-width:220px}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-accent-product{--tw-border-opacity: 1;border-color:rgb(108 99 255 / var(--tw-border-opacity, 1))}.border-accent-product\/20{border-color:#6c63ff33}.border-accent-product\/30{border-color:#6c63ff4d}.border-danger\/20{border-color:#c45b5b33}.border-primary-wash\/30{border-color:#8697c44d}.border-primary\/20{border-color:#3d52a033}.border-slate\/20{border-color:#4f5d7533}.border-success\/20{border-color:#7fb06933}.border-surface\/50{border-color:#adbbda80}.border-warning\/20{border-color:#d4a03c33}.border-warning\/30{border-color:#d4a03c4d}.border-warning\/40{border-color:#d4a03c66}.border-white\/10{border-color:#ffffff1a}.border-white\/20{border-color:#fff3}.border-white\/30{border-color:#ffffff4d}.border-t-transparent{border-top-color:transparent}.bg-accent-product{--tw-bg-opacity: 1;background-color:rgb(108 99 255 / var(--tw-bg-opacity, 1))}.bg-accent-product\/10{background-color:#6c63ff1a}.bg-background{--tw-bg-opacity: 1;background-color:rgb(237 232 245 / var(--tw-bg-opacity, 1))}.bg-black\/30{background-color:#0000004d}.bg-black\/5{background-color:#0000000d}.bg-charcoal\/10{background-color:#2d31421a}.bg-charcoal\/5{background-color:#2d31420d}.bg-charcoal\/\[0\.03\]{background-color:#2d314208}.bg-danger{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-danger\/10{background-color:#c45b5b1a}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(61 82 160 / var(--tw-bg-opacity, 1))}.bg-primary-light{--tw-bg-opacity: 1;background-color:rgb(112 145 230 / var(--tw-bg-opacity, 1))}.bg-primary-wash\/20{background-color:#8697c433}.bg-primary\/10{background-color:#3d52a01a}.bg-slate{--tw-bg-opacity: 1;background-color:rgb(79 93 117 / var(--tw-bg-opacity, 1))}.bg-slate\/10{background-color:#4f5d751a}.bg-slate\/30{background-color:#4f5d754d}.bg-slate\/40{background-color:#4f5d7566}.bg-success{--tw-bg-opacity: 1;background-color:rgb(127 176 105 / var(--tw-bg-opacity, 1))}.bg-success\/10{background-color:#7fb0691a}.bg-surface{--tw-bg-opacity: 1;background-color:rgb(173 187 218 / var(--tw-bg-opacity, 1))}.bg-surface\/30{background-color:#adbbda4d}.bg-surface\/50{background-color:#adbbda80}.bg-warning{--tw-bg-opacity: 1;background-color:rgb(212 160 60 / var(--tw-bg-opacity, 1))}.bg-warning\/10{background-color:#d4a03c1a}.bg-warning\/5{background-color:#d4a03c0d}.bg-white\/30{background-color:#ffffff4d}.bg-white\/40{background-color:#fff6}.fill-charcoal{fill:#2d3142}.fill-primary{fill:#3d52a0}.p-0{padding:0}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2\.5{padding-bottom:.625rem}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,Fira Code,Cascadia Code,monospace}.font-sans{font-family:Inter,system-ui,-apple-system,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-accent-product{--tw-text-opacity: 1;color:rgb(108 99 255 / var(--tw-text-opacity, 1))}.text-charcoal{--tw-text-opacity: 1;color:rgb(45 49 66 / var(--tw-text-opacity, 1))}.text-charcoal\/70{color:#2d3142b3}.text-charcoal\/80{color:#2d3142cc}.text-danger{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-primary{--tw-text-opacity: 1;color:rgb(61 82 160 / var(--tw-text-opacity, 1))}.text-primary-light{--tw-text-opacity: 1;color:rgb(112 145 230 / var(--tw-text-opacity, 1))}.text-primary-wash{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.text-slate{--tw-text-opacity: 1;color:rgb(79 93 117 / var(--tw-text-opacity, 1))}.text-slate\/40{color:#4f5d7566}.text-slate\/50{color:#4f5d7580}.text-slate\/60{color:#4f5d7599}.text-success{--tw-text-opacity: 1;color:rgb(127 176 105 / var(--tw-text-opacity, 1))}.text-warning{--tw-text-opacity: 1;color:rgb(212 160 60 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-glass{--tw-shadow: 0 8px 32px rgba(61,82,160,.08);--tw-shadow-colored: 0 8px 32px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-glass-subtle{--tw-shadow: 0 4px 16px rgba(61,82,160,.06);--tw-shadow-colored: 0 4px 16px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}.placeholder\:text-primary-wash::-moz-placeholder{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.placeholder\:text-primary-wash::placeholder{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.placeholder\:text-primary-wash\/70::-moz-placeholder{color:#8697c4b3}.placeholder\:text-primary-wash\/70::placeholder{color:#8697c4b3}.hover\:bg-accent-product\/20:hover{background-color:#6c63ff33}.hover\:bg-accent-product\/5:hover{background-color:#6c63ff0d}.hover\:bg-accent-product\/90:hover{background-color:#6c63ffe6}.hover\:bg-danger\/20:hover{background-color:#c45b5b33}.hover\:bg-primary\/5:hover{background-color:#3d52a00d}.hover\:bg-warning\/10:hover{background-color:#d4a03c1a}.hover\:bg-white\/20:hover{background-color:#fff3}.hover\:bg-white\/30:hover{background-color:#ffffff4d}.hover\:bg-white\/40:hover{background-color:#fff6}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:text-charcoal:hover{--tw-text-opacity: 1;color:rgb(45 49 66 / var(--tw-text-opacity, 1))}.hover\:text-primary-light:hover{--tw-text-opacity: 1;color:rgb(112 145 230 / var(--tw-text-opacity, 1))}.focus\:border-accent-product\/30:focus{border-color:#6c63ff4d}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-accent-product\/20:focus{--tw-ring-color: rgb(108 99 255 / .2)}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-accent-product{--tw-text-opacity: 1;color:rgb(108 99 255 / var(--tw-text-opacity, 1))}
@@ -8,8 +8,8 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
11
- <script type="module" crossorigin src="/assets/index-HYTmiwkW.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-DW1e50zX.css">
11
+ <script type="module" crossorigin src="/assets/index-Cm-odtQC.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-SenDDIyx.css">
13
13
  </head>
14
14
  <body class="bg-background text-charcoal font-sans antialiased">
15
15
  <div id="root"></div>
package/web-app/server.py CHANGED
@@ -69,6 +69,24 @@ class SessionState:
69
69
  self.log_lines: list[str] = []
70
70
  self.ws_clients: set[WebSocket] = set()
71
71
  self._reader_task: Optional[asyncio.Task] = None
72
+ self._lock = asyncio.Lock()
73
+
74
+ async def cleanup(self) -> None:
75
+ """Cancel reader task and close process pipes."""
76
+ if self._reader_task and not self._reader_task.done():
77
+ self._reader_task.cancel()
78
+ try:
79
+ await asyncio.wait_for(self._reader_task, timeout=3)
80
+ except (asyncio.CancelledError, asyncio.TimeoutError, Exception):
81
+ pass
82
+ self._reader_task = None
83
+
84
+ if self.process:
85
+ try:
86
+ if self.process.stdout:
87
+ self.process.stdout.close()
88
+ except Exception:
89
+ pass
72
90
 
73
91
  def reset(self) -> None:
74
92
  self.process = None
@@ -99,6 +117,9 @@ class StopResponse(BaseModel):
99
117
  message: str
100
118
 
101
119
 
120
+ _MAX_PRD_BYTES = 1_048_576 # 1 MB
121
+
122
+
102
123
  class PlanRequest(BaseModel):
103
124
  prd: str
104
125
  provider: str = "claude"
@@ -143,7 +164,7 @@ async def _broadcast(msg: dict) -> None:
143
164
  """Send a JSON message to all connected WebSocket clients."""
144
165
  data = json.dumps(msg)
145
166
  dead: list[WebSocket] = []
146
- for ws in session.ws_clients:
167
+ for ws in list(session.ws_clients):
147
168
  try:
148
169
  await ws.send_text(data)
149
170
  except Exception:
@@ -158,7 +179,7 @@ async def _read_process_output() -> None:
158
179
  if proc is None or proc.stdout is None:
159
180
  return
160
181
 
161
- loop = asyncio.get_event_loop()
182
+ loop = asyncio.get_running_loop()
162
183
 
163
184
  try:
164
185
  while session.running and proc.poll() is None:
@@ -223,72 +244,78 @@ async def start_session(req: StartRequest) -> JSONResponse:
223
244
  """Start a new loki session with the given PRD."""
224
245
  if len(req.prd.encode()) > _MAX_PRD_BYTES:
225
246
  return JSONResponse(status_code=400, content={"error": "PRD exceeds 1 MB limit"})
226
- if session.running:
227
- return JSONResponse(
228
- status_code=409,
229
- content={"error": "A session is already running. Stop it first."},
230
- )
231
-
232
- # Determine project directory
233
- project_dir = req.projectDir
234
- if not project_dir:
235
- project_dir = os.path.join(Path.home(), "purple-lab-projects", f"project-{int(time.time())}")
236
- os.makedirs(project_dir, exist_ok=True)
237
-
238
- # Write PRD to a temp file in the project dir
239
- prd_path = os.path.join(project_dir, "PRD.md")
240
- with open(prd_path, "w") as f:
241
- f.write(req.prd)
242
247
 
243
- # Build the loki start command
244
- if req.mode == "quick":
245
- # Extract first non-blank line as the task description
246
- first_line = next((l.strip() for l in req.prd.splitlines() if l.strip()), req.prd[:200])
247
- cmd = [
248
- str(LOKI_CLI),
249
- "quick",
250
- first_line,
251
- ]
252
- else:
253
- cmd = [
254
- str(LOKI_CLI),
255
- "start",
256
- "--provider", req.provider,
257
- prd_path,
258
- ]
259
-
260
- try:
261
- proc = subprocess.Popen(
262
- cmd,
263
- stdout=subprocess.PIPE,
264
- stderr=subprocess.STDOUT,
265
- stdin=subprocess.DEVNULL,
266
- text=True,
267
- cwd=project_dir,
268
- env={**os.environ, "LOKI_DIR": os.path.join(project_dir, ".loki")},
269
- )
270
- except FileNotFoundError:
271
- return JSONResponse(
272
- status_code=500,
273
- content={"error": f"loki CLI not found at {LOKI_CLI}"},
274
- )
275
- except Exception as e:
276
- return JSONResponse(
277
- status_code=500,
278
- content={"error": f"Failed to start session: {e}"},
279
- )
248
+ async with session._lock:
249
+ if session.running:
250
+ return JSONResponse(
251
+ status_code=409,
252
+ content={"error": "A session is already running. Stop it first."},
253
+ )
254
+
255
+ # Clean up any stale reader task from previous session
256
+ await session.cleanup()
257
+
258
+ # Determine project directory
259
+ project_dir = req.projectDir
260
+ if not project_dir:
261
+ project_dir = os.path.join(Path.home(), "purple-lab-projects", f"project-{int(time.time())}")
262
+ os.makedirs(project_dir, exist_ok=True)
263
+
264
+ # Write PRD to a temp file in the project dir
265
+ prd_path = os.path.join(project_dir, "PRD.md")
266
+ with open(prd_path, "w") as f:
267
+ f.write(req.prd)
268
+
269
+ # Build the loki start command
270
+ if req.mode == "quick":
271
+ # Extract first non-blank line as the task description
272
+ first_line = next((l.strip() for l in req.prd.splitlines() if l.strip()), req.prd[:200])
273
+ cmd = [
274
+ str(LOKI_CLI),
275
+ "quick",
276
+ first_line,
277
+ ]
278
+ else:
279
+ cmd = [
280
+ str(LOKI_CLI),
281
+ "start",
282
+ "--provider", req.provider,
283
+ prd_path,
284
+ ]
280
285
 
281
- # Update session state
282
- session.reset()
283
- session.process = proc
284
- session.running = True
285
- session.provider = req.provider
286
- session.prd_text = req.prd
287
- session.project_dir = project_dir
288
- session.start_time = time.time()
286
+ try:
287
+ proc = subprocess.Popen(
288
+ cmd,
289
+ stdout=subprocess.PIPE,
290
+ stderr=subprocess.STDOUT,
291
+ stdin=subprocess.DEVNULL,
292
+ text=True,
293
+ cwd=project_dir,
294
+ env={**os.environ, "LOKI_DIR": os.path.join(project_dir, ".loki")},
295
+ start_new_session=True, # create new process group for clean kill
296
+ )
297
+ except FileNotFoundError:
298
+ return JSONResponse(
299
+ status_code=500,
300
+ content={"error": f"loki CLI not found at {LOKI_CLI}"},
301
+ )
302
+ except Exception as e:
303
+ return JSONResponse(
304
+ status_code=500,
305
+ content={"error": f"Failed to start session: {e}"},
306
+ )
307
+
308
+ # Update session state
309
+ session.reset()
310
+ session.process = proc
311
+ session.running = True
312
+ session.provider = req.provider
313
+ session.prd_text = req.prd
314
+ session.project_dir = project_dir
315
+ session.start_time = time.time()
289
316
 
290
- # Start background output reader
291
- session._reader_task = asyncio.create_task(_read_process_output())
317
+ # Start background output reader
318
+ session._reader_task = asyncio.create_task(_read_process_output())
292
319
 
293
320
  await _broadcast({"type": "session_start", "data": {
294
321
  "provider": req.provider,
@@ -307,24 +334,48 @@ async def start_session(req: StartRequest) -> JSONResponse:
307
334
  @app.post("/api/session/stop")
308
335
  async def stop_session() -> JSONResponse:
309
336
  """Stop the current loki session."""
310
- if not session.running or session.process is None:
311
- return JSONResponse(content={"stopped": False, "message": "No session running"})
337
+ async with session._lock:
338
+ if not session.running or session.process is None:
339
+ return JSONResponse(content={"stopped": False, "message": "No session running"})
340
+
341
+ proc = session.process
342
+
343
+ # 1. Mark stopped first so reader task loop exits
344
+ session.running = False
345
+
346
+ # 2. Cancel reader task before killing process
347
+ await session.cleanup()
348
+
349
+ # 3. Kill the process group (catches child processes too)
350
+ try:
351
+ pgid = os.getpgid(proc.pid)
352
+ os.killpg(pgid, signal.SIGTERM)
353
+ except (ProcessLookupError, PermissionError, OSError):
354
+ # Fallback: kill the process directly
355
+ try:
356
+ proc.terminate()
357
+ except Exception:
358
+ pass
312
359
 
313
- try:
314
- # Send SIGTERM, then SIGKILL after 5s
315
- session.process.terminate()
316
360
  try:
317
- session.process.wait(timeout=5)
361
+ proc.wait(timeout=5)
318
362
  except subprocess.TimeoutExpired:
319
- session.process.kill()
320
- session.process.wait(timeout=3)
321
- except Exception:
322
- pass
363
+ try:
364
+ pgid = os.getpgid(proc.pid)
365
+ os.killpg(pgid, signal.SIGKILL)
366
+ except (ProcessLookupError, PermissionError, OSError):
367
+ try:
368
+ proc.kill()
369
+ except Exception:
370
+ pass
371
+ try:
372
+ proc.wait(timeout=3)
373
+ except Exception:
374
+ pass
323
375
 
324
- session.running = False
325
- await _broadcast({"type": "session_end", "data": {"message": "Session stopped by user"}})
376
+ await _broadcast({"type": "session_end", "data": {"message": "Session stopped by user"}})
326
377
 
327
- return JSONResponse(content={"stopped": True, "message": "Session stopped"})
378
+ return JSONResponse(content={"stopped": True, "message": "Session stopped"})
328
379
 
329
380
 
330
381
  @app.get("/api/session/status")
@@ -607,9 +658,6 @@ def _run_loki_cmd(args: list, cwd: Optional[str] = None, timeout: int = 60) -> t
607
658
  return (1, str(e))
608
659
 
609
660
 
610
- _MAX_PRD_BYTES = 1_048_576 # 1 MB
611
-
612
-
613
661
  @app.post("/api/session/plan")
614
662
  async def plan_session(req: PlanRequest) -> JSONResponse:
615
663
  """Run loki plan dry-run analysis and return structured result."""
@@ -620,7 +668,7 @@ async def plan_session(req: PlanRequest) -> JSONResponse:
620
668
  f.write(req.prd)
621
669
  prd_tmp = f.name
622
670
  try:
623
- rc, output = await asyncio.get_event_loop().run_in_executor(
671
+ rc, output = await asyncio.get_running_loop().run_in_executor(
624
672
  None, lambda: _run_loki_cmd(["plan", prd_tmp], timeout=90)
625
673
  )
626
674
  finally:
@@ -671,7 +719,7 @@ async def plan_session(req: PlanRequest) -> JSONResponse:
671
719
  async def generate_report(req: ReportRequest) -> JSONResponse:
672
720
  """Run loki report and return content."""
673
721
  fmt = req.format if req.format in ("html", "markdown") else "markdown"
674
- rc, output = await asyncio.get_event_loop().run_in_executor(
722
+ rc, output = await asyncio.get_running_loop().run_in_executor(
675
723
  None, lambda: _run_loki_cmd(["report", "--format", fmt], timeout=60)
676
724
  )
677
725
  return JSONResponse(content={
@@ -684,7 +732,7 @@ async def generate_report(req: ReportRequest) -> JSONResponse:
684
732
  @app.post("/api/session/share")
685
733
  async def share_session() -> JSONResponse:
686
734
  """Run loki share and return Gist URL."""
687
- rc, output = await asyncio.get_event_loop().run_in_executor(
735
+ rc, output = await asyncio.get_running_loop().run_in_executor(
688
736
  None, lambda: _run_loki_cmd(["share"], timeout=60)
689
737
  )
690
738
  # Try to extract URL from output
@@ -748,7 +796,7 @@ async def set_provider(req: ProviderSetRequest) -> JSONResponse:
748
796
  @app.get("/api/session/metrics")
749
797
  async def get_metrics() -> JSONResponse:
750
798
  """Run loki metrics --json and return parsed output."""
751
- rc, output = await asyncio.get_event_loop().run_in_executor(
799
+ rc, output = await asyncio.get_running_loop().run_in_executor(
752
800
  None, lambda: _run_loki_cmd(["metrics", "--json"], timeout=30)
753
801
  )
754
802
  # Try JSON parse
@@ -782,14 +830,67 @@ async def get_metrics() -> JSONResponse:
782
830
  return JSONResponse(content=metrics)
783
831
 
784
832
 
833
+ def _infer_session_status(entry: Path) -> str:
834
+ """Infer session status from project directory contents."""
835
+ # 1. Check .loki/state/session.json for explicit phase
836
+ state_file = entry / ".loki" / "state" / "session.json"
837
+ if state_file.exists():
838
+ try:
839
+ with open(state_file) as f:
840
+ st = json.load(f)
841
+ phase = st.get("phase", "")
842
+ if phase and phase != "idle":
843
+ return phase
844
+ except (json.JSONDecodeError, OSError):
845
+ pass
846
+
847
+ # 2. Check .loki/autonomy-state.json (run.sh writes this)
848
+ for state_name in ("autonomy-state.json", ".loki/autonomy-state.json"):
849
+ sf = entry / state_name
850
+ if sf.exists():
851
+ try:
852
+ with open(sf) as f:
853
+ st = json.load(f)
854
+ if st.get("completed") or st.get("status") == "completed":
855
+ return "completed"
856
+ if st.get("status"):
857
+ return st["status"]
858
+ except (json.JSONDecodeError, OSError):
859
+ pass
860
+
861
+ # 3. Infer from file contents
862
+ files = set()
863
+ try:
864
+ files = {f.name for f in entry.iterdir() if not f.name.startswith(".")}
865
+ except OSError:
866
+ pass
867
+
868
+ source_extensions = {".js", ".ts", ".tsx", ".py", ".html", ".css", ".go", ".rs", ".java", ".rb"}
869
+ has_source = any(
870
+ (entry / f).suffix in source_extensions
871
+ for f in files
872
+ if (entry / f).is_file()
873
+ )
874
+ has_prd = "PRD.md" in files or "prd.md" in files
875
+
876
+ if has_source:
877
+ return "completed"
878
+ if has_prd and len(files) <= 2:
879
+ return "started"
880
+ if has_prd:
881
+ return "in_progress"
882
+
883
+ return "empty"
884
+
885
+
785
886
  @app.get("/api/sessions/history")
786
887
  async def get_sessions_history() -> JSONResponse:
787
- """Return list of past loki sessions from ~/.loki-sessions/ or ~/.loki/sessions/."""
888
+ """Return list of past loki sessions from ~/purple-lab-projects/ etc."""
788
889
  history: list[dict] = []
789
890
  search_dirs = [
891
+ Path.home() / "purple-lab-projects",
790
892
  Path.home() / ".loki-sessions",
791
893
  Path.home() / ".loki" / "sessions",
792
- Path.home() / "purple-lab-projects",
793
894
  ]
794
895
  for base_dir in search_dirs:
795
896
  if not base_dir.is_dir():
@@ -802,7 +903,7 @@ async def get_sessions_history() -> JSONResponse:
802
903
  "path": str(entry),
803
904
  "date": "",
804
905
  "prd_snippet": "",
805
- "status": "unknown",
906
+ "status": _infer_session_status(entry),
806
907
  }
807
908
  # Read timestamp from directory mtime
808
909
  try:
@@ -821,21 +922,83 @@ async def get_sessions_history() -> JSONResponse:
821
922
  except OSError:
822
923
  pass
823
924
  break
824
- # Try to read status
825
- state_file = entry / ".loki" / "state" / "session.json"
826
- if state_file.exists():
827
- try:
828
- with open(state_file) as f:
829
- st = json.load(f)
830
- session_info["status"] = st.get("phase", "unknown")
831
- except (json.JSONDecodeError, OSError):
832
- pass
925
+ # Count project files for progress indication
926
+ try:
927
+ file_count = sum(1 for f in entry.rglob("*") if f.is_file()
928
+ and ".git" not in f.parts and "node_modules" not in f.parts
929
+ and "__pycache__" not in f.parts)
930
+ session_info["file_count"] = file_count
931
+ except OSError:
932
+ session_info["file_count"] = 0
933
+
833
934
  history.append(session_info)
834
935
  if history:
835
936
  break # Use first directory that has entries
836
937
  return JSONResponse(content=history)
837
938
 
838
939
 
940
+ @app.get("/api/sessions/{session_id}")
941
+ async def get_session_detail(session_id: str) -> JSONResponse:
942
+ """Get details of a past session for read-only viewing."""
943
+ import re
944
+ # Validate session_id format (prevent path traversal)
945
+ if not re.match(r"^[a-zA-Z0-9_-]+$", session_id):
946
+ return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
947
+
948
+ search_dirs = [
949
+ Path.home() / "purple-lab-projects",
950
+ Path.home() / ".loki-sessions",
951
+ Path.home() / ".loki" / "sessions",
952
+ ]
953
+ target: Optional[Path] = None
954
+ for base_dir in search_dirs:
955
+ candidate = base_dir / session_id
956
+ if candidate.is_dir():
957
+ target = candidate
958
+ break
959
+
960
+ if target is None:
961
+ return JSONResponse(status_code=404, content={"error": "Session not found"})
962
+
963
+ # Read PRD
964
+ prd_content = ""
965
+ for prd_name in ("PRD.md", "prd.md", ".loki/prd.md"):
966
+ prd_file = target / prd_name
967
+ if prd_file.exists():
968
+ try:
969
+ prd_content = prd_file.read_text(errors="replace")
970
+ except OSError:
971
+ pass
972
+ break
973
+
974
+ # Build file tree
975
+ files = _build_file_tree(target)
976
+
977
+ # Read logs if available
978
+ log_lines: list[str] = []
979
+ for log_name in (".loki/logs/session.log", ".loki/session.log", "loki.log"):
980
+ log_file = target / log_name
981
+ if log_file.exists():
982
+ try:
983
+ text = log_file.read_text(errors="replace")
984
+ log_lines = text.splitlines()[-200:]
985
+ except OSError:
986
+ pass
987
+ break
988
+
989
+ # Status
990
+ status = _infer_session_status(target)
991
+
992
+ return JSONResponse(content={
993
+ "id": session_id,
994
+ "path": str(target),
995
+ "status": status,
996
+ "prd": prd_content,
997
+ "files": files,
998
+ "logs": log_lines,
999
+ })
1000
+
1001
+
839
1002
  @app.post("/api/session/onboard")
840
1003
  async def onboard_session(req: OnboardRequest) -> JSONResponse:
841
1004
  """Run loki onboard on a path and return CLAUDE.md content."""
@@ -852,7 +1015,7 @@ async def onboard_session(req: OnboardRequest) -> JSONResponse:
852
1015
  if not target.is_dir():
853
1016
  return JSONResponse(status_code=400, content={"error": "Path must be a directory"})
854
1017
 
855
- rc, output = await asyncio.get_event_loop().run_in_executor(
1018
+ rc, output = await asyncio.get_running_loop().run_in_executor(
856
1019
  None, lambda: _run_loki_cmd(["onboard", str(target)], cwd=str(target), timeout=120)
857
1020
  )
858
1021
  # Try to read generated CLAUDE.md
@@ -870,6 +1033,17 @@ async def onboard_session(req: OnboardRequest) -> JSONResponse:
870
1033
  })
871
1034
 
872
1035
 
1036
+ # ---------------------------------------------------------------------------
1037
+ # Health check
1038
+ # ---------------------------------------------------------------------------
1039
+
1040
+
1041
+ @app.get("/health")
1042
+ async def health_check() -> JSONResponse:
1043
+ """Health check for load balancers and orchestrators."""
1044
+ return JSONResponse(content={"status": "ok", "service": "purple-lab"})
1045
+
1046
+
873
1047
  # ---------------------------------------------------------------------------
874
1048
  # WebSocket
875
1049
  # ---------------------------------------------------------------------------
@@ -1011,6 +1185,10 @@ async def websocket_endpoint(ws: WebSocket) -> None:
1011
1185
  pass
1012
1186
  finally:
1013
1187
  push_task.cancel()
1188
+ try:
1189
+ await push_task
1190
+ except (asyncio.CancelledError, Exception):
1191
+ pass
1014
1192
  session.ws_clients.discard(ws)
1015
1193
 
1016
1194
 
@@ -1 +0,0 @@
1
- *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,-apple-system,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,Fira Code,Cascadia Code,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.terminal-scroll::-webkit-scrollbar{width:6px}.terminal-scroll::-webkit-scrollbar-track{background:transparent}.terminal-scroll::-webkit-scrollbar-thumb{background:#3d52a033;border-radius:3px}.terminal-scroll::-webkit-scrollbar-thumb:hover{background:#3d52a059}.glass{background:#fff9;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,.3);border-radius:16px;box-shadow:0 8px 32px #3d52a014}.glass-subtle{background:#fff6;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.2);border-radius:12px;box-shadow:0 4px 16px #3d52a00f}.pattern-circles{position:absolute;top:0;right:0;bottom:0;left:0;opacity:.04;background-image:radial-gradient(circle at 20% 30%,transparent 30%,#6C63FF 30.5%,transparent 31%),radial-gradient(circle at 80% 70%,transparent 25%,#6C63FF 25.5%,transparent 26%),radial-gradient(circle at 50% 50%,transparent 40%,#6C63FF 40.5%,transparent 41%);pointer-events:none}@keyframes phase-pulse{0%,to{opacity:1}50%{opacity:.5}}.phase-active{animation:phase-pulse 2s ease-in-out infinite}@keyframes cursor-blink{0%,to{opacity:1}50%{opacity:0}}.terminal-cursor:after{content:"";display:inline-block;width:8px;height:16px;background:#7091e6;animation:cursor-blink 1s step-end infinite;margin-left:2px;vertical-align:text-bottom}.\!visible{visibility:visible!important}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.right-0{right:0}.top-0{top:0}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-5{grid-column:span 5 / span 5}.col-span-6{grid-column:span 6 / span 6}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-28{height:7rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-\[400px\]{max-height:400px}.min-h-0{min-height:0px}.min-h-\[280px\]{min-height:280px}.min-h-\[300px\]{min-height:300px}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-56{width:14rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.max-w-3xl{max-width:48rem}.max-w-\[1920px\]{max-width:1920px}.max-w-\[220px\]{max-width:220px}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-accent-product{--tw-border-opacity: 1;border-color:rgb(108 99 255 / var(--tw-border-opacity, 1))}.border-accent-product\/20{border-color:#6c63ff33}.border-accent-product\/30{border-color:#6c63ff4d}.border-danger\/20{border-color:#c45b5b33}.border-primary-wash\/30{border-color:#8697c44d}.border-primary\/20{border-color:#3d52a033}.border-slate\/20{border-color:#4f5d7533}.border-success\/20{border-color:#7fb06933}.border-surface\/50{border-color:#adbbda80}.border-warning\/20{border-color:#d4a03c33}.border-warning\/30{border-color:#d4a03c4d}.border-warning\/40{border-color:#d4a03c66}.border-white\/10{border-color:#ffffff1a}.border-white\/20{border-color:#fff3}.border-white\/30{border-color:#ffffff4d}.border-t-transparent{border-top-color:transparent}.bg-accent-product{--tw-bg-opacity: 1;background-color:rgb(108 99 255 / var(--tw-bg-opacity, 1))}.bg-accent-product\/10{background-color:#6c63ff1a}.bg-background{--tw-bg-opacity: 1;background-color:rgb(237 232 245 / var(--tw-bg-opacity, 1))}.bg-black\/30{background-color:#0000004d}.bg-black\/5{background-color:#0000000d}.bg-charcoal\/10{background-color:#2d31421a}.bg-charcoal\/5{background-color:#2d31420d}.bg-charcoal\/\[0\.03\]{background-color:#2d314208}.bg-danger{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-danger\/10{background-color:#c45b5b1a}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(61 82 160 / var(--tw-bg-opacity, 1))}.bg-primary-light{--tw-bg-opacity: 1;background-color:rgb(112 145 230 / var(--tw-bg-opacity, 1))}.bg-primary-wash\/20{background-color:#8697c433}.bg-primary\/10{background-color:#3d52a01a}.bg-slate{--tw-bg-opacity: 1;background-color:rgb(79 93 117 / var(--tw-bg-opacity, 1))}.bg-slate\/10{background-color:#4f5d751a}.bg-slate\/30{background-color:#4f5d754d}.bg-slate\/40{background-color:#4f5d7566}.bg-success{--tw-bg-opacity: 1;background-color:rgb(127 176 105 / var(--tw-bg-opacity, 1))}.bg-success\/10{background-color:#7fb0691a}.bg-surface{--tw-bg-opacity: 1;background-color:rgb(173 187 218 / var(--tw-bg-opacity, 1))}.bg-surface\/30{background-color:#adbbda4d}.bg-surface\/50{background-color:#adbbda80}.bg-warning{--tw-bg-opacity: 1;background-color:rgb(212 160 60 / var(--tw-bg-opacity, 1))}.bg-warning\/10{background-color:#d4a03c1a}.bg-warning\/5{background-color:#d4a03c0d}.bg-white\/30{background-color:#ffffff4d}.bg-white\/40{background-color:#fff6}.fill-charcoal{fill:#2d3142}.fill-primary{fill:#3d52a0}.p-0{padding:0}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2\.5{padding-bottom:.625rem}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,Fira Code,Cascadia Code,monospace}.font-sans{font-family:Inter,system-ui,-apple-system,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-accent-product{--tw-text-opacity: 1;color:rgb(108 99 255 / var(--tw-text-opacity, 1))}.text-charcoal{--tw-text-opacity: 1;color:rgb(45 49 66 / var(--tw-text-opacity, 1))}.text-charcoal\/80{color:#2d3142cc}.text-danger{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-primary{--tw-text-opacity: 1;color:rgb(61 82 160 / var(--tw-text-opacity, 1))}.text-primary-light{--tw-text-opacity: 1;color:rgb(112 145 230 / var(--tw-text-opacity, 1))}.text-primary-wash{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.text-slate{--tw-text-opacity: 1;color:rgb(79 93 117 / var(--tw-text-opacity, 1))}.text-slate\/40{color:#4f5d7566}.text-slate\/50{color:#4f5d7580}.text-slate\/60{color:#4f5d7599}.text-success{--tw-text-opacity: 1;color:rgb(127 176 105 / var(--tw-text-opacity, 1))}.text-warning{--tw-text-opacity: 1;color:rgb(212 160 60 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-glass{--tw-shadow: 0 8px 32px rgba(61,82,160,.08);--tw-shadow-colored: 0 8px 32px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-glass-subtle{--tw-shadow: 0 4px 16px rgba(61,82,160,.06);--tw-shadow-colored: 0 4px 16px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}.placeholder\:text-primary-wash::-moz-placeholder{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.placeholder\:text-primary-wash::placeholder{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.placeholder\:text-primary-wash\/70::-moz-placeholder{color:#8697c4b3}.placeholder\:text-primary-wash\/70::placeholder{color:#8697c4b3}.hover\:bg-accent-product\/20:hover{background-color:#6c63ff33}.hover\:bg-accent-product\/5:hover{background-color:#6c63ff0d}.hover\:bg-accent-product\/90:hover{background-color:#6c63ffe6}.hover\:bg-danger\/20:hover{background-color:#c45b5b33}.hover\:bg-primary\/5:hover{background-color:#3d52a00d}.hover\:bg-warning\/10:hover{background-color:#d4a03c1a}.hover\:bg-white\/20:hover{background-color:#fff3}.hover\:bg-white\/30:hover{background-color:#ffffff4d}.hover\:bg-white\/40:hover{background-color:#fff6}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:text-charcoal:hover{--tw-text-opacity: 1;color:rgb(45 49 66 / var(--tw-text-opacity, 1))}.hover\:text-primary-light:hover{--tw-text-opacity: 1;color:rgb(112 145 230 / var(--tw-text-opacity, 1))}.focus\:border-accent-product\/30:focus{border-color:#6c63ff4d}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-accent-product\/20:focus{--tw-ring-color: rgb(108 99 255 / .2)}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-accent-product{--tw-text-opacity: 1;color:rgb(108 99 255 / var(--tw-text-opacity, 1))}