railscope 0.1.2 → 0.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a891dab66be8ea0c685b4edbf8ac6089426d0f8bc48159cefcd8e709afb5f0d
4
- data.tar.gz: f0a40a9df1579a6fd37e8c77861d4a07a9f0155794c4440fe23bb7bf55251c05
3
+ metadata.gz: 365fa77a20cf2375e26da9f92c52dae76d20ff8cce31a793f921e7e392208149
4
+ data.tar.gz: 838667bc52bf55f90c2372d713d4677407675f2ec8b27cdcc460eb701ea89931
5
5
  SHA512:
6
- metadata.gz: badbe24c5d96dd3ae5be0c51e57db45e0c83cef4a7fdeb79c2640a50c7fd591b5f0ecc973e79fe9ca996361cace6765f2fd0027200733f3750559df8f3b2c3c1
7
- data.tar.gz: 72c8ce099532400ebec6cf4b5c22b862ec8ab1fd742b087a69db1d1ead106d4f0a61a0ef2e7030c135359de5d7936d3eff92ef404077e15c92df1bd4566a955d
6
+ metadata.gz: 25d34d2449ae7cd8c9b0940438a93de8357cb4f5a73b85fab3f762bbd46cb0a41f87e905c474d467b15330f83ce7c35f3e7d9adbaf4a3bd88a8f0c0f8ee683b1
7
+ data.tar.gz: 392dfb08fe6a2fbda012364e0a0e705a913337626e4fef890deb1bff469cb009ee68e8bb851988e8814a7e7f578cd04df6295e0f821dd5364205b337af2c30b8
@@ -64,14 +64,27 @@ function JsonValue({ value, indent = 0 }: { value: unknown; indent?: number }) {
64
64
  }
65
65
  return (
66
66
  <div>
67
- {entries.map(([key, val], i) => (
68
- <div key={key} className="flex">
69
- <span className="text-blue-400">"{key}"</span>
70
- <span className="text-dark-muted mr-2">:</span>
71
- <JsonValue value={val} indent={indent + 1} />
72
- {i < entries.length - 1 && <span className="text-dark-muted">,</span>}
73
- </div>
74
- ))}
67
+ <span className="text-dark-muted">{'{'}</span>
68
+ <div className="pl-4">
69
+ {entries.map(([key, val], i) => {
70
+ const isComplex = val !== null && typeof val === 'object'
71
+ return (
72
+ <div key={key}>
73
+ <span className="text-blue-400">"{key}"</span>
74
+ <span className="text-dark-muted mr-1">:</span>
75
+ {isComplex ? (
76
+ <JsonValue value={val} indent={indent + 1} />
77
+ ) : (
78
+ <>
79
+ <JsonValue value={val} indent={indent + 1} />
80
+ </>
81
+ )}
82
+ {i < entries.length - 1 && <span className="text-dark-muted">,</span>}
83
+ </div>
84
+ )
85
+ })}
86
+ </div>
87
+ <span className="text-dark-muted">{'}'}</span>
75
88
  </div>
76
89
  )
77
90
  }
@@ -20,6 +20,21 @@ Railscope.configure do |config|
20
20
  #
21
21
  # config.enabled = true
22
22
 
23
+ # Storage Backend
24
+ # ---------------
25
+ # :database - Direct writes to PostgreSQL (simpler, no Redis needed)
26
+ # :redis - Buffer in Redis, batch flush to PostgreSQL (faster requests)
27
+ #
28
+ # When using :redis, entries are buffered in Redis during requests and
29
+ # flushed to PostgreSQL periodically via Railscope::FlushService.
30
+ # You can trigger the flush with:
31
+ # - Railscope::FlushService.call (from a job, cron, etc.)
32
+ # - rake railscope:flush
33
+ #
34
+ # Can also be set via RAILSCOPE_STORAGE env var.
35
+ #
36
+ # config.storage_backend = :database
37
+
23
38
  # Retention Period
24
39
  # ----------------
25
40
  # Number of days to keep entries before purging.
@@ -78,7 +78,7 @@ module Railscope
78
78
  end
79
79
 
80
80
  rake_tasks do
81
- # Load sample tasks for testing
81
+ load Railscope::Engine.root.join("lib/tasks/railscope.rake")
82
82
  load Railscope::Engine.root.join("lib/tasks/railscope_sample.rake")
83
83
 
84
84
  # Subscribe to rake tasks after they're loaded
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railscope
4
+ class FlushService
5
+ BUFFER_KEY = "railscope:buffer"
6
+ UPDATES_KEY = "railscope:buffer:updates"
7
+ BATCH_SIZE = 1000
8
+
9
+ def self.call
10
+ new.call
11
+ end
12
+
13
+ def call
14
+ total = flush_entries
15
+ apply_pending_updates
16
+ total
17
+ end
18
+
19
+ private
20
+
21
+ def flush_entries
22
+ total = 0
23
+
24
+ loop do
25
+ batch = pop_batch(BUFFER_KEY)
26
+ break if batch.empty?
27
+
28
+ entries = batch.map { |json| JSON.parse(json, symbolize_names: true) }
29
+ batch_insert_to_database(entries)
30
+ total += entries.size
31
+ end
32
+
33
+ total
34
+ end
35
+
36
+ def apply_pending_updates
37
+ loop do
38
+ batch = pop_batch(UPDATES_KEY)
39
+ break if batch.empty?
40
+
41
+ batch.each do |json|
42
+ update = JSON.parse(json, symbolize_names: true)
43
+ apply_update(update)
44
+ end
45
+ end
46
+ end
47
+
48
+ def apply_update(update)
49
+ entry = Entry.where(
50
+ batch_id: update[:batch_id],
51
+ entry_type: update[:entry_type].to_s
52
+ ).order(created_at: :desc).first
53
+ return unless entry
54
+
55
+ entry.payload = entry.payload.merge(
56
+ update[:payload_updates].transform_keys(&:to_s)
57
+ )
58
+ entry.save!
59
+ rescue StandardError => e
60
+ Rails.logger.debug("[Railscope] Failed to apply buffered update: #{e.message}")
61
+ end
62
+
63
+ def batch_insert_to_database(entries)
64
+ now = Time.current
65
+
66
+ records = entries.map do |entry|
67
+ {
68
+ uuid: entry[:uuid],
69
+ batch_id: entry[:batch_id],
70
+ family_hash: entry[:family_hash],
71
+ entry_type: entry[:entry_type],
72
+ payload: entry[:payload],
73
+ tags: entry[:tags] || [],
74
+ should_display_on_index: entry.fetch(:should_display_on_index, true),
75
+ occurred_at: entry[:occurred_at] || now,
76
+ created_at: entry[:created_at] || now,
77
+ updated_at: entry[:updated_at] || now
78
+ }
79
+ end
80
+
81
+ Entry.insert_all(records)
82
+ end
83
+
84
+ def pop_batch(key)
85
+ redis.lpop(key, BATCH_SIZE) || []
86
+ end
87
+
88
+ def redis
89
+ Railscope.redis
90
+ end
91
+ end
92
+ end
@@ -80,20 +80,9 @@ module Railscope
80
80
  end
81
81
  session_data = extract_session_from_env(context_data[:env])
82
82
 
83
- # Parse JSON if applicable
83
+ # Determine response type and handle accordingly (like Telescope)
84
84
  content_type = response_headers["Content-Type"] || response_headers["content-type"] || ""
85
- looks_like_json = response_body.to_s.start_with?("{", "[")
86
- is_json = content_type.include?("application/json") || looks_like_json
87
-
88
- parsed_body = if is_json
89
- begin
90
- JSON.parse(response_body)
91
- rescue StandardError
92
- response_body
93
- end
94
- else
95
- response_body
96
- end
85
+ parsed_body = parse_response_body(response_body, content_type, context_data[:env])
97
86
 
98
87
  payload_updates = {
99
88
  "response" => parsed_body.presence,
@@ -101,24 +90,129 @@ module Railscope
101
90
  "session" => session_data
102
91
  }
103
92
 
104
- storage = Railscope.storage
93
+ Railscope.storage.update_by_batch(
94
+ batch_id: context_data[:batch_id],
95
+ entry_type: "request",
96
+ payload_updates: payload_updates
97
+ )
98
+ rescue StandardError => e
99
+ Rails.logger.debug("[Railscope] Failed to update entry with response: #{e.message}")
100
+ end
105
101
 
106
- if storage.is_a?(Railscope::Storage::Database)
107
- entry = Entry.where(batch_id: context_data[:batch_id], entry_type: "request").order(created_at: :desc).first
108
- return unless entry
102
+ def self.parse_response_body(response_body, content_type, env)
103
+ body_str = response_body.to_s
109
104
 
110
- entry.payload = entry.payload.merge(payload_updates)
111
- entry.save!
105
+ # Empty response
106
+ return "Empty Response" if body_str.blank?
112
107
 
113
- elsif storage.respond_to?(:update_by_batch)
114
- storage.update_by_batch(
115
- batch_id: context_data[:batch_id],
116
- entry_type: "request",
117
- payload_updates: payload_updates
118
- )
108
+ # Redirect
109
+ if env && (location = env["action_dispatch.redirect_url"])
110
+ return "Redirected to #{location}"
119
111
  end
120
- rescue StandardError => e
121
- Rails.logger.debug("[Railscope] Failed to update entry with response: #{e.message}")
112
+
113
+ # JSON response
114
+ if content_type.include?("application/json") || body_str.match?(/\A\s*[\[{]/)
115
+ begin
116
+ return JSON.parse(body_str)
117
+ rescue StandardError
118
+ return body_str.truncate(2000)
119
+ end
120
+ end
121
+
122
+ # Plain text response
123
+ if content_type.include?("text/plain")
124
+ return body_str.truncate(2000)
125
+ end
126
+
127
+ # HTML view response — extract template path and data like Telescope
128
+ extract_view_response(env) || "HTML Response"
129
+ end
130
+
131
+ def self.extract_view_response(env)
132
+ return nil unless env
133
+
134
+ controller = env["action_controller.instance"]
135
+ return nil unless controller
136
+
137
+ view_path = resolve_view_path(controller)
138
+ return nil unless view_path
139
+
140
+ {
141
+ "view" => view_path,
142
+ "data" => extract_controller_data(controller)
143
+ }
144
+ rescue StandardError
145
+ nil
146
+ end
147
+
148
+ def self.resolve_view_path(controller)
149
+ # Try to get the actual rendered template from the controller
150
+ if controller.respond_to?(:rendered_format, true) || controller.respond_to?(:controller_path)
151
+ template_path = "app/views/#{controller.controller_path}/#{controller.action_name}"
152
+
153
+ # Try to find the actual file with extension
154
+ if defined?(Rails.root)
155
+ candidates = Dir.glob(Rails.root.join("#{template_path}.*"))
156
+ return candidates.first&.sub("#{Rails.root}/", "") if candidates.any?
157
+ end
158
+
159
+ template_path
160
+ end
161
+ rescue StandardError
162
+ nil
163
+ end
164
+
165
+ def self.extract_controller_data(controller)
166
+ data = {}
167
+
168
+ controller.instance_variables.each do |ivar|
169
+ name = ivar.to_s.delete_prefix("@")
170
+
171
+ # Skip internal Rails/controller variables
172
+ next if name.start_with?("_")
173
+ next if IGNORED_INSTANCE_VARS.include?(name)
174
+
175
+ value = controller.instance_variable_get(ivar)
176
+ data[name] = safe_serialize(value)
177
+ end
178
+
179
+ data.presence || {}
180
+ rescue StandardError
181
+ {}
182
+ end
183
+
184
+ IGNORED_INSTANCE_VARS = %w[
185
+ request response marked_for_same_origin_verification
186
+ performed_redirect action_has_layout lookup_context
187
+ view_context_class current_renderer view_renderer
188
+ action_name pressed_key action_status response_body
189
+ ].freeze
190
+
191
+ def self.safe_serialize(value, depth: 0)
192
+ return "..." if depth > 5
193
+
194
+ case value
195
+ when String, Numeric, TrueClass, FalseClass, NilClass
196
+ value
197
+ when Symbol
198
+ value.to_s
199
+ when Time, DateTime
200
+ value.iso8601
201
+ when Date
202
+ value.to_s
203
+ when ActiveRecord::Relation
204
+ value.limit(50).map { |record| safe_serialize(record, depth: depth + 1) }
205
+ when ActiveRecord::Base
206
+ Railscope.filter(value.attributes.transform_values { |v| safe_serialize(v, depth: depth + 1) })
207
+ when Array
208
+ value.first(50).map { |v| safe_serialize(v, depth: depth + 1) }
209
+ when Hash
210
+ value.transform_values { |v| safe_serialize(v, depth: depth + 1) }
211
+ else
212
+ value.to_s
213
+ end
214
+ rescue StandardError
215
+ value.class.name
122
216
  end
123
217
 
124
218
  def self.extract_session_from_env(env)
@@ -10,6 +10,15 @@ module Railscope
10
10
  EntryData.from_active_record(record)
11
11
  end
12
12
 
13
+ def update_by_batch(batch_id:, entry_type:, payload_updates:)
14
+ entry = Entry.where(batch_id: batch_id, entry_type: entry_type).order(created_at: :desc).first
15
+ return nil unless entry
16
+
17
+ entry.payload = entry.payload.merge(payload_updates)
18
+ entry.save!
19
+ EntryData.from_active_record(entry)
20
+ end
21
+
13
22
  def find(uuid)
14
23
  record = Entry.find_by(uuid: uuid)
15
24
  return nil unless record
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railscope
4
+ module Storage
5
+ class RedisBuffer < Base
6
+ BUFFER_KEY = "railscope:buffer"
7
+ UPDATES_KEY = "railscope:buffer:updates"
8
+
9
+ # WRITE → Redis (fast, ~0.1ms)
10
+ def write(attributes)
11
+ entry = build_entry(attributes)
12
+ redis.rpush(BUFFER_KEY, entry.to_json)
13
+ entry
14
+ end
15
+
16
+ # UPDATE → Redis buffer (for response data from middleware)
17
+ def update_by_batch(batch_id:, entry_type:, payload_updates:)
18
+ update = {
19
+ batch_id: batch_id,
20
+ entry_type: entry_type,
21
+ payload_updates: payload_updates
22
+ }
23
+ redis.rpush(UPDATES_KEY, update.to_json)
24
+ end
25
+
26
+ # READ → Database (source of truth)
27
+ def find(uuid)
28
+ database_adapter.find(uuid)
29
+ end
30
+
31
+ def all(filters: {}, page: 1, per_page: 25, displayable_only: true)
32
+ database_adapter.all(filters: filters, page: page, per_page: per_page, displayable_only: displayable_only)
33
+ end
34
+
35
+ def count(filters: {}, displayable_only: true)
36
+ database_adapter.count(filters: filters, displayable_only: displayable_only)
37
+ end
38
+
39
+ def for_batch(batch_id)
40
+ database_adapter.for_batch(batch_id)
41
+ end
42
+
43
+ def for_family(family_hash, page: 1, per_page: 25)
44
+ database_adapter.for_family(family_hash, page: page, per_page: per_page)
45
+ end
46
+
47
+ def family_count(family_hash)
48
+ database_adapter.family_count(family_hash)
49
+ end
50
+
51
+ def destroy_all!
52
+ redis.del(BUFFER_KEY, UPDATES_KEY)
53
+ database_adapter.destroy_all!
54
+ end
55
+
56
+ def destroy_expired!
57
+ database_adapter.destroy_expired!
58
+ end
59
+
60
+ # READY → needs both Redis AND database
61
+ def ready?
62
+ Railscope.redis_available? && database_adapter.ready?
63
+ end
64
+
65
+ private
66
+
67
+ def redis
68
+ Railscope.redis
69
+ end
70
+
71
+ def database_adapter
72
+ @database_adapter ||= Database.new
73
+ end
74
+
75
+ def build_entry(attributes)
76
+ now = Time.current
77
+ EntryData.new(
78
+ uuid: attributes[:uuid] || SecureRandom.uuid,
79
+ batch_id: attributes[:batch_id],
80
+ family_hash: attributes[:family_hash],
81
+ entry_type: attributes[:entry_type],
82
+ payload: attributes[:payload] || {},
83
+ tags: attributes[:tags] || [],
84
+ should_display_on_index: attributes.fetch(:should_display_on_index, true),
85
+ occurred_at: attributes[:occurred_at] || now,
86
+ created_at: attributes[:created_at] || now,
87
+ updated_at: attributes[:updated_at] || now
88
+ )
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Railscope
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/railscope.rb CHANGED
@@ -6,7 +6,8 @@ require_relative "railscope/filter"
6
6
  require_relative "railscope/entry_data"
7
7
  require_relative "railscope/storage/base"
8
8
  require_relative "railscope/storage/database"
9
- require_relative "railscope/storage/redis_storage"
9
+ require_relative "railscope/storage/redis_buffer"
10
+ require_relative "railscope/flush_service"
10
11
  require_relative "railscope/middleware"
11
12
  require_relative "railscope/engine"
12
13
 
@@ -57,7 +58,7 @@ module Railscope
57
58
  def storage
58
59
  @storage ||= case storage_backend
59
60
  when STORAGE_REDIS
60
- Storage::RedisStorage.new
61
+ Storage::RedisBuffer.new
61
62
  else
62
63
  Storage::Database.new
63
64
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :railscope do
4
+ desc "Flush buffered entries from Redis to database"
5
+ task flush: :environment do
6
+ count = Railscope::FlushService.call
7
+ puts "Flushed #{count} entries"
8
+ end
9
+
10
+ desc "Purge expired entries"
11
+ task purge: :environment do
12
+ count = Railscope.storage.destroy_expired!
13
+ puts "Purged #{count} entries"
14
+ end
15
+ end
@@ -1 +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:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";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:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,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}.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.inset-y-0{top:0;bottom:0}.left-0{left:0}.right-0{right:0}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.h-16{height:4rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-screen{height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-28{width:7rem}.w-32{width:8rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.min-w-\[3rem\]{min-width:3rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-dark-border>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(48 54 61 / var(--tw-divide-opacity, 1))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-0{border-width:0px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-dark-border{--tw-border-opacity: 1;border-color:rgb(48 54 61 / var(--tw-border-opacity, 1))}.border-dark-border\/50{border-color:#30363d80}.bg-\[\#1a1a2e\]{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.bg-black\/20{background-color:#0003}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-500\/20{background-color:#3b82f633}.bg-dark-bg{--tw-bg-opacity: 1;background-color:rgb(13 17 23 / var(--tw-bg-opacity, 1))}.bg-dark-border{--tw-bg-opacity: 1;background-color:rgb(48 54 61 / var(--tw-bg-opacity, 1))}.bg-dark-border\/50{background-color:#30363d80}.bg-dark-surface{--tw-bg-opacity: 1;background-color:rgb(22 27 34 / var(--tw-bg-opacity, 1))}.bg-gray-500\/20{background-color:#6b728033}.bg-green-500\/20{background-color:#22c55e33}.bg-purple-500\/20{background-color:#a855f733}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/20{background-color:#ef444433}.bg-red-500\/30{background-color:#ef44444d}.bg-transparent{background-color:transparent}.bg-yellow-500\/20{background-color:#eab30833}.p-0{padding:0}.p-2{padding:.5rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.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-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.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-8{padding-top:2rem;padding-bottom:2rem}.pl-10{padding-left:2.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pr-3{padding-right:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.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}.italic{font-style:italic}.tracking-wide{letter-spacing:.025em}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-dark-muted{--tw-text-opacity: 1;color:rgb(139 148 158 / var(--tw-text-opacity, 1))}.text-dark-text{--tw-text-opacity: 1;color:rgb(201 209 217 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.placeholder-dark-muted::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(139 148 158 / var(--tw-placeholder-opacity, 1))}.placeholder-dark-muted::placeholder{--tw-placeholder-opacity: 1;color:rgb(139 148 158 / var(--tw-placeholder-opacity, 1))}.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)}.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}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;background-color:#0d1117;color:#c9d1d9}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#161b22}::-webkit-scrollbar-thumb{background:#30363d;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#484f58}.hover\:bg-dark-border:hover{--tw-bg-opacity: 1;background-color:rgb(48 54 61 / var(--tw-bg-opacity, 1))}.hover\:bg-dark-border\/50:hover{background-color:#30363d80}.hover\:bg-white\/\[0\.02\]:hover{background-color:#ffffff05}.hover\:text-blue-300:hover{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.hover\:text-dark-text:hover{--tw-text-opacity: 1;color:rgb(201 209 217 / var(--tw-text-opacity, 1))}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1: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(1px + 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-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 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:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";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:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,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}.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.inset-y-0{top:0;bottom:0}.left-0{left:0}.right-0{right:0}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mr-1{margin-right:.25rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.h-16{height:4rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-screen{height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-28{width:7rem}.w-32{width:8rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.min-w-\[3rem\]{min-width:3rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-dark-border>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(48 54 61 / var(--tw-divide-opacity, 1))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-0{border-width:0px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-dark-border{--tw-border-opacity: 1;border-color:rgb(48 54 61 / var(--tw-border-opacity, 1))}.border-dark-border\/50{border-color:#30363d80}.bg-\[\#1a1a2e\]{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.bg-black\/20{background-color:#0003}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-500\/20{background-color:#3b82f633}.bg-dark-bg{--tw-bg-opacity: 1;background-color:rgb(13 17 23 / var(--tw-bg-opacity, 1))}.bg-dark-border{--tw-bg-opacity: 1;background-color:rgb(48 54 61 / var(--tw-bg-opacity, 1))}.bg-dark-border\/50{background-color:#30363d80}.bg-dark-surface{--tw-bg-opacity: 1;background-color:rgb(22 27 34 / var(--tw-bg-opacity, 1))}.bg-gray-500\/20{background-color:#6b728033}.bg-green-500\/20{background-color:#22c55e33}.bg-purple-500\/20{background-color:#a855f733}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/20{background-color:#ef444433}.bg-red-500\/30{background-color:#ef44444d}.bg-transparent{background-color:transparent}.bg-yellow-500\/20{background-color:#eab30833}.p-0{padding:0}.p-2{padding:.5rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.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-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.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-8{padding-top:2rem;padding-bottom:2rem}.pl-10{padding-left:2.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pr-3{padding-right:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.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}.italic{font-style:italic}.tracking-wide{letter-spacing:.025em}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-dark-muted{--tw-text-opacity: 1;color:rgb(139 148 158 / var(--tw-text-opacity, 1))}.text-dark-text{--tw-text-opacity: 1;color:rgb(201 209 217 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.placeholder-dark-muted::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(139 148 158 / var(--tw-placeholder-opacity, 1))}.placeholder-dark-muted::placeholder{--tw-placeholder-opacity: 1;color:rgb(139 148 158 / var(--tw-placeholder-opacity, 1))}.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)}.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}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;background-color:#0d1117;color:#c9d1d9}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#161b22}::-webkit-scrollbar-thumb{background:#30363d;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#484f58}.hover\:bg-dark-border:hover{--tw-bg-opacity: 1;background-color:rgb(48 54 61 / var(--tw-bg-opacity, 1))}.hover\:bg-dark-border\/50:hover{background-color:#30363d80}.hover\:bg-white\/\[0\.02\]:hover{background-color:#ffffff05}.hover\:text-blue-300:hover{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.hover\:text-dark-text:hover{--tw-text-opacity: 1;color:rgb(201 209 217 / var(--tw-text-opacity, 1))}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1: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(1px + 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-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}