lionscope 0.1.1 → 0.2.0

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.
@@ -23533,6 +23533,8 @@ function isValidPackageName(value) {
23533
23533
  return value.length <= 255 && PACKAGE_NAME_PATTERN.test(value);
23534
23534
  }
23535
23535
  var MAX_FRAME_DURATIONS = 240;
23536
+ var STALL_THRESHOLDS_MS = [100, 150, 250, 500, 1e3];
23537
+ var DEFAULT_SDK_OPTIONS = { stallThresholdMs: 150, strictMode: false };
23536
23538
  var FRAME_STAGES = [
23537
23539
  "input",
23538
23540
  "animation",
@@ -23544,6 +23546,7 @@ var FRAME_STAGES = [
23544
23546
  "delay",
23545
23547
  "gpu"
23546
23548
  ];
23549
+ var MAX_STACK_FRAMES = 64;
23547
23550
  var CONTROL_SERVER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
23548
23551
  "hello",
23549
23552
  "devices",
@@ -23553,8 +23556,9 @@ var CONTROL_SERVER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
23553
23556
  "error"
23554
23557
  ]);
23555
23558
  var MAX_CLIENT_MESSAGE_BYTES = 4096;
23556
- var DEVICE_PROTOCOL_VERSION = 1;
23559
+ var DEVICE_PROTOCOL_VERSION = 2;
23557
23560
  var MIN_DEVICE_PROTOCOL_VERSION = 1;
23561
+ var DEVICE_PROTOCOL_V2 = 2;
23558
23562
  var MAX_DEVICE_PROTOCOL_VERSION = 1e3;
23559
23563
  var DEVICE_LINK_PORT = DEFAULT_DEVICE_GATEWAY_PORT;
23560
23564
  var MAX_DEVICE_LINE_BYTES = 64 * 1024;
@@ -23567,7 +23571,15 @@ var clientMessageSchema = external_exports.discriminatedUnion("type", [
23567
23571
  external_exports.object({ type: external_exports.literal("refreshDevices") }).strict(),
23568
23572
  external_exports.object({ type: external_exports.literal("listApps"), serial: serialSchema }).strict(),
23569
23573
  external_exports.object({ type: external_exports.literal("startSession"), serial: serialSchema, packageName: packageNameSchema }).strict(),
23570
- external_exports.object({ type: external_exports.literal("stopSession") }).strict()
23574
+ external_exports.object({ type: external_exports.literal("stopSession") }).strict(),
23575
+ external_exports.object({
23576
+ type: external_exports.literal("setSdkOptions"),
23577
+ stallThresholdMs: external_exports.literal(STALL_THRESHOLDS_MS).optional(),
23578
+ strictMode: external_exports.boolean().optional()
23579
+ }).strict().refine(
23580
+ (message) => message.stallThresholdMs !== void 0 || message.strictMode !== void 0,
23581
+ "no SDK option given"
23582
+ )
23571
23583
  ]);
23572
23584
  var utf8 = new TextEncoder();
23573
23585
  function readJson(raw, maxBytes) {
@@ -23622,6 +23634,20 @@ var sdkFrameSchema = external_exports.object({
23622
23634
  deadlineMs: durationMs.nullable(),
23623
23635
  firstDraw: external_exports.boolean()
23624
23636
  }).strip();
23637
+ var MAX_STACK_LINE = 2147483647;
23638
+ var MAX_STALL_DURATION_MS = 6e4;
23639
+ var MAX_STALL_SAMPLES = 1e4;
23640
+ var MAX_STRICT_MODE_COUNT = 1e6;
23641
+ var CONTROL_OR_FORMAT_RUNS = /[\p{Cc}\p{Cf}]+/gu;
23642
+ var sanitized = (value) => value.replace(CONTROL_OR_FORMAT_RUNS, " ").trim();
23643
+ var stackFrameSchema = external_exports.object({
23644
+ className: text,
23645
+ method: text,
23646
+ file: textOf(128).nullable(),
23647
+ // -2 marks a native method; -1 (unknown) is tolerated although the SDK sends null for it.
23648
+ line: external_exports.number().int().min(-2).max(MAX_STACK_LINE).nullable(),
23649
+ inApp: external_exports.boolean()
23650
+ }).strip();
23625
23651
  var deviceMessageSchema = external_exports.discriminatedUnion("type", [
23626
23652
  external_exports.object({
23627
23653
  type: external_exports.literal("hello"),
@@ -23643,30 +23669,67 @@ var deviceMessageSchema = external_exports.discriminatedUnion("type", [
23643
23669
  frames: external_exports.array(sdkFrameSchema).max(MAX_SDK_FRAMES_PER_BATCH),
23644
23670
  droppedFrames: external_exports.number().int().nonnegative().max(MAX_SDK_DROPPED_FRAMES).optional()
23645
23671
  }).strip(),
23646
- external_exports.object({ type: external_exports.literal("heartbeat"), ts: epochMs, droppedMessages: external_exports.number().int().nonnegative() }).strip()
23672
+ external_exports.object({ type: external_exports.literal("heartbeat"), ts: epochMs, droppedMessages: external_exports.number().int().nonnegative() }).strip(),
23673
+ external_exports.object({
23674
+ type: external_exports.literal("stall"),
23675
+ ts: epochMs,
23676
+ durationMs: external_exports.number().nonnegative().max(MAX_STALL_DURATION_MS),
23677
+ frames: external_exports.array(stackFrameSchema).max(MAX_STACK_FRAMES),
23678
+ samples: external_exports.number().int().min(1).max(MAX_STALL_SAMPLES),
23679
+ truncated: external_exports.boolean(),
23680
+ screen: text.nullable(),
23681
+ debugger: external_exports.boolean()
23682
+ }).strip(),
23683
+ external_exports.object({
23684
+ type: external_exports.literal("strictmode"),
23685
+ ts: epochMs,
23686
+ policy: external_exports.enum(["thread", "vm"]),
23687
+ // Free text from the app's StrictMode violation: cleaned up rather than refused. An anonymous violation
23688
+ // class has an empty simple name.
23689
+ violation: external_exports.string().max(128).transform((value) => sanitized(value) || "Violation"),
23690
+ message: external_exports.string().max(MAX_SDK_TEXT_LENGTH).transform((value) => sanitized(value) || null).nullable(),
23691
+ frames: external_exports.array(stackFrameSchema).max(MAX_STACK_FRAMES),
23692
+ count: external_exports.number().int().min(1).max(MAX_STRICT_MODE_COUNT),
23693
+ screen: text.nullable()
23694
+ }).strip()
23647
23695
  ]);
23648
- var DEVICE_MESSAGE_TYPES = {
23649
- hello: true,
23650
- screen: true,
23651
- frames: true,
23652
- heartbeat: true
23696
+ var DIAGNOSTIC_TYPES = /* @__PURE__ */ new Set(["stall", "strictmode"]);
23697
+ var DEVICE_MESSAGE_VERSIONS = {
23698
+ hello: MIN_DEVICE_PROTOCOL_VERSION,
23699
+ screen: MIN_DEVICE_PROTOCOL_VERSION,
23700
+ frames: MIN_DEVICE_PROTOCOL_VERSION,
23701
+ heartbeat: MIN_DEVICE_PROTOCOL_VERSION,
23702
+ stall: DEVICE_PROTOCOL_V2,
23703
+ strictmode: DEVICE_PROTOCOL_V2
23653
23704
  };
23705
+ function isKnownType(type) {
23706
+ return Object.hasOwn(DEVICE_MESSAGE_VERSIONS, type);
23707
+ }
23654
23708
  function isSupportedProtocolVersion(version2) {
23655
23709
  return Number.isInteger(version2) && version2 >= MIN_DEVICE_PROTOCOL_VERSION && version2 <= MAX_DEVICE_PROTOCOL_VERSION;
23656
23710
  }
23657
- function parseDeviceMessage(line) {
23711
+ function parseDeviceMessage(line, protocolVersion = DEVICE_PROTOCOL_VERSION) {
23658
23712
  const json2 = readJson(line, MAX_DEVICE_LINE_BYTES);
23659
23713
  if (!json2.ok) return json2;
23660
23714
  const object2 = typeof json2.value === "object" && json2.value !== null ? json2.value : null;
23661
- const type = object2 && Reflect.get(object2, "type");
23662
- if (typeof type === "string" && !Object.hasOwn(DEVICE_MESSAGE_TYPES, type)) {
23663
- return { ok: false, error: "unknown message type", unknownType: type };
23715
+ const rawType = object2 && Reflect.get(object2, "type");
23716
+ let type = null;
23717
+ if (typeof rawType === "string") {
23718
+ if (!isKnownType(rawType)) return { ok: false, error: "unknown message type", unknownType: rawType };
23719
+ type = rawType;
23720
+ }
23721
+ const since = type === null ? MIN_DEVICE_PROTOCOL_VERSION : DEVICE_MESSAGE_VERSIONS[type];
23722
+ if (type !== null && since > protocolVersion) {
23723
+ return { ok: false, error: `${type} needs protocol version ${since}`, unnegotiated: { type, since } };
23664
23724
  }
23665
23725
  const version2 = object2 && type === "hello" ? Reflect.get(object2, "protocolVersion") : void 0;
23666
23726
  if (typeof version2 === "number" && !isSupportedProtocolVersion(version2)) {
23667
23727
  return { ok: false, error: `unsupported protocol version ${version2}`, unsupportedVersion: version2 };
23668
23728
  }
23669
- return validate2(json2.value, deviceMessageSchema);
23729
+ const result = validate2(json2.value, deviceMessageSchema);
23730
+ if (!result.ok && type !== null && DIAGNOSTIC_TYPES.has(type))
23731
+ return { ...result, invalidDiagnostic: type };
23732
+ return result;
23670
23733
  }
23671
23734
 
23672
23735
  // server/src/config.ts
@@ -23870,6 +23933,8 @@ function createDeviceGateway(options) {
23870
23933
  const warnings = new ErrorThrottle();
23871
23934
  let address = null;
23872
23935
  let ignoredMessages = 0;
23936
+ let unnegotiatedMessages = 0;
23937
+ let invalidDiagnostics = 0;
23873
23938
  const warn = (reason) => {
23874
23939
  if (warnings.shouldReport(reason)) log.warn(`Device gateway: closed a connection (${reason})`);
23875
23940
  };
@@ -23881,6 +23946,20 @@ function createDeviceGateway(options) {
23881
23946
  `Device gateway: ignoring SDK messages of unknown type "${shown}" (a newer SDK?), ${ignoredMessages} so far`
23882
23947
  );
23883
23948
  };
23949
+ const skipUnnegotiated = (type, since, negotiated) => {
23950
+ unnegotiatedMessages += 1;
23951
+ if (!warnings.shouldReport("unnegotiated type")) return;
23952
+ log.warn(
23953
+ `Device gateway: ignoring "${type}": SDK sent a v${since} message on a v${negotiated} link, ${unnegotiatedMessages} so far`
23954
+ );
23955
+ };
23956
+ const dropDiagnostic = (type, error62) => {
23957
+ invalidDiagnostics += 1;
23958
+ if (!warnings.shouldReport("invalid diagnostic")) return;
23959
+ log.warn(
23960
+ `Device gateway: dropped an invalid SDK ${type} message (${error62.slice(0, 200)}), ${invalidDiagnostics} so far`
23961
+ );
23962
+ };
23884
23963
  const server = createServer((socket) => {
23885
23964
  socket.on("error", () => void 0);
23886
23965
  if (handshaking.size >= limits.maxHandshakes) {
@@ -23902,7 +23981,16 @@ function createDeviceGateway(options) {
23902
23981
  linked.add(socket);
23903
23982
  return null;
23904
23983
  };
23905
- serveConnection(socket, { target: options.target, limits, now, warn, ignore, admit });
23984
+ serveConnection(socket, {
23985
+ target: options.target,
23986
+ limits,
23987
+ now,
23988
+ warn,
23989
+ ignore,
23990
+ skipUnnegotiated,
23991
+ dropDiagnostic,
23992
+ admit
23993
+ });
23906
23994
  });
23907
23995
  return {
23908
23996
  get address() {
@@ -23928,18 +24016,26 @@ function createDeviceGateway(options) {
23928
24016
  }
23929
24017
  };
23930
24018
  }
24019
+ function gatewayLine(message) {
24020
+ return `${JSON.stringify(message)}
24021
+ `;
24022
+ }
24023
+ function configMessage(options) {
24024
+ return { type: "config", stallThresholdMs: options.stallThresholdMs, strictMode: options.strictMode };
24025
+ }
23931
24026
  function endSocket(socket, message) {
23932
- if (message) socket.end(`${JSON.stringify(message)}
23933
- `);
24027
+ if (message) socket.end(gatewayLine(message));
23934
24028
  else socket.end();
23935
24029
  setTimeout(() => socket.destroy(), CLOSE_GRACE_MS).unref();
23936
24030
  }
23937
24031
  function serveConnection(socket, context) {
23938
- const { target, limits, warn, ignore } = context;
24032
+ const { target, limits, warn, ignore, skipUnnegotiated, dropDiagnostic } = context;
23939
24033
  const bucket = new TokenBucket(limits.lineBurst, limits.linesPerSecond, context.now);
23940
24034
  let partial2 = [];
23941
24035
  let partialBytes = 0;
23942
24036
  let handler = null;
24037
+ let protocolVersion = 0;
24038
+ let pendingConfig = null;
23943
24039
  let closing = false;
23944
24040
  const close = (bye) => {
23945
24041
  if (closing) return;
@@ -23967,13 +24063,23 @@ function serveConnection(socket, context) {
23967
24063
  warn("too many SDK connections");
23968
24064
  return close(refused);
23969
24065
  }
23970
- const protocolVersion = Math.min(parsed.value.protocolVersion, DEVICE_PROTOCOL_VERSION);
23971
- const result = target.attachSdk(parsed.value, { close, protocolVersion });
24066
+ protocolVersion = Math.min(parsed.value.protocolVersion, DEVICE_PROTOCOL_VERSION);
24067
+ const link = {
24068
+ protocolVersion,
24069
+ close,
24070
+ sendConfig: (options) => {
24071
+ if (protocolVersion < DEVICE_PROTOCOL_V2 || closing) return;
24072
+ if (handler) socket.write(gatewayLine(configMessage(options)));
24073
+ else pendingConfig = configMessage(options);
24074
+ }
24075
+ };
24076
+ const result = target.attachSdk(parsed.value, link);
23972
24077
  if (!result.ok) return close(result.reason);
23973
24078
  handler = result.handler;
23974
- const welcome = { type: "welcome", protocolVersion };
23975
- socket.write(`${JSON.stringify(welcome)}
23976
- `);
24079
+ if (closing) return;
24080
+ socket.write(gatewayLine({ type: "welcome", protocolVersion }));
24081
+ if (pendingConfig) socket.write(gatewayLine(pendingConfig));
24082
+ pendingConfig = null;
23977
24083
  };
23978
24084
  const onLine = (raw) => {
23979
24085
  if (!bucket.take()) {
@@ -23982,8 +24088,15 @@ function serveConnection(socket, context) {
23982
24088
  }
23983
24089
  const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
23984
24090
  if (!handler) return onHello(line);
23985
- const parsed = parseDeviceMessage(line);
24091
+ const parsed = parseDeviceMessage(line, protocolVersion);
23986
24092
  if (!parsed.ok && parsed.unknownType !== void 0) return ignore(parsed.unknownType);
24093
+ if (!parsed.ok && parsed.unnegotiated) {
24094
+ return skipUnnegotiated(parsed.unnegotiated.type, parsed.unnegotiated.since, protocolVersion);
24095
+ }
24096
+ if (!parsed.ok && parsed.invalidDiagnostic !== void 0) {
24097
+ dropDiagnostic(parsed.invalidDiagnostic, parsed.error);
24098
+ return handler.onInvalidDiagnostic();
24099
+ }
23987
24100
  if (!parsed.ok || parsed.value.type === "hello") {
23988
24101
  warn("invalid message");
23989
24102
  return close("protocol_error");
@@ -24206,8 +24319,10 @@ var PUBLIC_FAILURES = {
24206
24319
  refreshDevices: "Could not list devices",
24207
24320
  listApps: "Could not list apps on the device",
24208
24321
  startSession: "Could not start the profiling session",
24209
- stopSession: "Could not stop the profiling session"
24322
+ stopSession: "Could not stop the profiling session",
24323
+ setSdkOptions: "Could not change the SDK options"
24210
24324
  };
24325
+ var NO_SESSION_MESSAGE = "No profiling session: start one before changing SDK options";
24211
24326
  function createWsGateway(options) {
24212
24327
  const { server, manager } = options;
24213
24328
  const wss = new import_websocket_server.default({ noServer: true, maxPayload: MAX_CLIENT_MESSAGE_BYTES });
@@ -24257,6 +24372,13 @@ function createWsGateway(options) {
24257
24372
  case "stopSession":
24258
24373
  send(ws, { type: "session", session: await manager.stopSession() });
24259
24374
  return;
24375
+ case "setSdkOptions": {
24376
+ const { stallThresholdMs, strictMode } = message;
24377
+ if (!manager.setSdkOptions({ stallThresholdMs, strictMode })) {
24378
+ send(ws, { type: "error", code: "no_session", message: NO_SESSION_MESSAGE });
24379
+ }
24380
+ return;
24381
+ }
24260
24382
  }
24261
24383
  };
24262
24384
  const onMessage = (ws, state, data, isBinary) => {
@@ -24561,6 +24683,12 @@ var TimeWindowBuffer = class {
24561
24683
  // server/src/store/sessionStore.ts
24562
24684
  var MAX_SAMPLES = 5e3;
24563
24685
  var MAX_SCREEN_SPANS = 2e3;
24686
+ var MAX_EVENTS = 2e3;
24687
+ var MAX_EVENT_STREAM_BYTES = 2 * 1024 * 1024;
24688
+ var eventLimit = {
24689
+ maxBytes: MAX_EVENT_STREAM_BYTES,
24690
+ sizeOf: (item) => Buffer.byteLength(JSON.stringify(item))
24691
+ };
24564
24692
  var STREAMS = {
24565
24693
  cpu: { maxItems: MAX_SAMPLES, toMessages: (items) => items.map((sample) => ({ type: "cpu", sample })) },
24566
24694
  memory: {
@@ -24586,6 +24714,16 @@ var STREAMS = {
24586
24714
  maxItems: MAX_SAMPLES,
24587
24715
  increasing: true,
24588
24716
  toMessages: (items) => items.map((sample) => ({ type: "breakdown", sample }))
24717
+ },
24718
+ stalls: {
24719
+ maxItems: MAX_EVENTS,
24720
+ byteLimit: eventLimit,
24721
+ toMessages: (items) => items.map((sample) => ({ type: "stall", sample }))
24722
+ },
24723
+ strictMode: {
24724
+ maxItems: MAX_EVENTS,
24725
+ byteLimit: eventLimit,
24726
+ toMessages: (items) => items.map((sample) => ({ type: "strictmode", sample }))
24589
24727
  }
24590
24728
  };
24591
24729
  var SAMPLE_STREAMS = Object.keys(STREAMS);
@@ -24607,7 +24745,9 @@ var SessionStore = class {
24607
24745
  frames: buffer("frames"),
24608
24746
  logs: buffer("logs"),
24609
24747
  events: buffer("events"),
24610
- breakdown: buffer("breakdown")
24748
+ breakdown: buffer("breakdown"),
24749
+ stalls: buffer("stalls"),
24750
+ strictMode: buffer("strictMode")
24611
24751
  };
24612
24752
  this.screens = new TimeWindowBuffer(options.windowMs, MAX_SCREEN_SPANS);
24613
24753
  }
@@ -25148,12 +25288,19 @@ var MAX_FRAME_DURATION_MS = 6e4;
25148
25288
  var MAX_HELD_MESSAGES = 16;
25149
25289
  var droppedFramesMessage = (count) => `SDK reported ${count} dropped frames (device busy); frame counts may read low`;
25150
25290
  var SDK_CLOCK_MESSAGE = "SDK frames dropped: the device clock does not match the laptop clock";
25291
+ var MAX_EVENT_AGE_MS = 6e4;
25292
+ var MAX_EVENT_LEAD_MS = MAX_FRAME_LEAD_MS;
25293
+ var INVALID_DIAGNOSTIC_MESSAGE = "SDK sent an invalid diagnostic; ignored";
25294
+ var CONFIG_DEBOUNCE_MS = 150;
25295
+ var SDK_EVENT_CLOCK_MESSAGE = "SDK stalls / StrictMode violations dropped: the device clock does not match the laptop clock";
25151
25296
  var DISCONNECTED_SDK = {
25152
25297
  connected: false,
25153
25298
  sdkVersion: null,
25154
25299
  processName: null,
25155
25300
  apiLevel: null,
25156
- framesActive: false
25301
+ framesActive: false,
25302
+ protocolVersion: null,
25303
+ options: DEFAULT_SDK_OPTIONS
25157
25304
  };
25158
25305
  var SdkBridge = class {
25159
25306
  constructor(options) {
@@ -25170,13 +25317,33 @@ var SdkBridge = class {
25170
25317
  /** Frames FrameMetrics dropped before the SDK saw them, summed over the session. */
25171
25318
  droppedFrames = 0;
25172
25319
  drainTimer = null;
25320
+ configTimer = null;
25173
25321
  stopped = false;
25174
25322
  get status() {
25175
25323
  return this.current;
25176
25324
  }
25325
+ /**
25326
+ * Change the SDK options (partial): announced with the status at once and, when they changed, pushed to the
25327
+ * links CONFIG_DEBOUNCE_MS after the last change.
25328
+ */
25329
+ setOptions(changes) {
25330
+ if (this.stopped) return;
25331
+ const previous = this.sdkOptions;
25332
+ const options = {
25333
+ stallThresholdMs: changes.stallThresholdMs ?? previous.stallThresholdMs,
25334
+ strictMode: changes.strictMode ?? previous.strictMode
25335
+ };
25336
+ this.update({ options });
25337
+ if (options.stallThresholdMs === previous.stallThresholdMs && options.strictMode === previous.strictMode) {
25338
+ return;
25339
+ }
25340
+ if (this.configTimer) clearTimeout(this.configTimer);
25341
+ this.configTimer = setTimeout(() => this.pushConfig(), CONFIG_DEBOUNCE_MS);
25342
+ }
25177
25343
  attach(hello, link) {
25178
25344
  const entry = { hello, link, held: [] };
25179
25345
  this.pending.add(entry);
25346
+ link.sendConfig(this.sdkOptions);
25180
25347
  void this.verify(entry);
25181
25348
  return {
25182
25349
  onMessage: (message) => {
@@ -25184,12 +25351,17 @@ var SdkBridge = class {
25184
25351
  if (this.active === entry) this.onMessage(message);
25185
25352
  else if (this.pending.has(entry) && entry.held.length < MAX_HELD_MESSAGES) entry.held.push(message);
25186
25353
  },
25354
+ onInvalidDiagnostic: () => {
25355
+ if (this.active === entry && !this.stopped) {
25356
+ this.options.notify("sdk:invalid-diagnostic", INVALID_DIAGNOSTIC_MESSAGE);
25357
+ }
25358
+ },
25187
25359
  onClose: () => {
25188
25360
  this.pending.delete(entry);
25189
25361
  if (this.active !== entry || this.stopped) return;
25190
25362
  this.active = null;
25191
25363
  this.release();
25192
- this.update({ connected: false, framesActive: false });
25364
+ this.update({ connected: false, framesActive: false, protocolVersion: null });
25193
25365
  }
25194
25366
  };
25195
25367
  }
@@ -25197,13 +25369,20 @@ var SdkBridge = class {
25197
25369
  stop() {
25198
25370
  if (this.stopped) return;
25199
25371
  this.stopped = true;
25372
+ if (this.configTimer) clearTimeout(this.configTimer);
25373
+ this.configTimer = null;
25200
25374
  for (const entry of this.pending) entry.link.close("no_session");
25201
25375
  this.pending.clear();
25202
25376
  const active = this.active;
25203
25377
  this.active = null;
25204
25378
  this.release();
25205
25379
  active?.link.close("no_session");
25206
- this.current = { ...this.current, connected: false, framesActive: false };
25380
+ this.current = { ...this.current, connected: false, framesActive: false, protocolVersion: null };
25381
+ }
25382
+ pushConfig() {
25383
+ this.configTimer = null;
25384
+ const links = this.active ? [...this.pending, this.active] : [...this.pending];
25385
+ for (const entry of links) entry.link.sendConfig(this.sdkOptions);
25207
25386
  }
25208
25387
  async verify(entry) {
25209
25388
  const { pid } = entry.hello;
@@ -25226,7 +25405,8 @@ var SdkBridge = class {
25226
25405
  connected: true,
25227
25406
  sdkVersion: entry.hello.sdkVersion,
25228
25407
  processName: entry.hello.processName,
25229
- apiLevel: entry.hello.apiLevel
25408
+ apiLevel: entry.hello.apiLevel,
25409
+ protocolVersion: entry.link.protocolVersion
25230
25410
  });
25231
25411
  for (const message of entry.held.splice(0)) this.onMessage(message);
25232
25412
  }
@@ -25263,8 +25443,65 @@ var SdkBridge = class {
25263
25443
  );
25264
25444
  }
25265
25445
  return;
25446
+ case "stall": {
25447
+ const ts = this.eventTime(message.ts, message.durationMs);
25448
+ if (ts === null) return;
25449
+ const { durationMs: durationMs2, frames, samples, truncated, debugger: debuggerAttached } = message;
25450
+ this.options.store.append("stalls", {
25451
+ ts,
25452
+ durationMs: durationMs2,
25453
+ frames,
25454
+ samples,
25455
+ truncated,
25456
+ screen: this.screenAt(ts, message.screen),
25457
+ debugger: debuggerAttached
25458
+ });
25459
+ return;
25460
+ }
25461
+ case "strictmode": {
25462
+ const ts = this.eventTime(message.ts, 0);
25463
+ if (ts === null) return;
25464
+ const { policy, violation, count, frames } = message;
25465
+ this.options.store.append("strictMode", {
25466
+ ts,
25467
+ policy,
25468
+ violation,
25469
+ message: message.message,
25470
+ frames,
25471
+ count,
25472
+ screen: this.screenAt(ts, message.screen)
25473
+ });
25474
+ return;
25475
+ }
25266
25476
  }
25267
25477
  }
25478
+ /**
25479
+ * Laptop time of a stall / StrictMode event, or null when it lies outside the accepted window (reported) or
25480
+ * ended before the session started (dropped quietly). One that straddles the session start begins at it, like
25481
+ * screen spans; its duration is kept.
25482
+ */
25483
+ eventTime(deviceTs, durationMs2) {
25484
+ const now = this.options.now();
25485
+ const ts = this.toLaptop(deviceTs);
25486
+ const lead = ts - (now + MAX_EVENT_LEAD_MS);
25487
+ const age = ts + durationMs2 - (now - MAX_EVENT_AGE_MS);
25488
+ if (lead <= 0 && age >= 0) {
25489
+ const { startedAt } = this.options;
25490
+ return ts + durationMs2 < startedAt ? null : Math.max(startedAt, Math.min(ts, now));
25491
+ }
25492
+ if (this.refusals.shouldReport("event-clock")) {
25493
+ const skew = lead > 0 ? `${Math.round(lead)} ms ahead of` : `${Math.round(-age)} ms behind`;
25494
+ log.warn(
25495
+ `${this.options.logPrefix} dropped an SDK stall/StrictMode event ${skew} the accepted window (clock offset ${this.options.clockOffsetMs()} ms)`
25496
+ );
25497
+ }
25498
+ this.options.notify("sdk:event-clock", SDK_EVENT_CLOCK_MESSAGE);
25499
+ return null;
25500
+ }
25501
+ /** The SDK's own screen name, else the top screen of the recorded spans. */
25502
+ screenAt(ts, sdkScreen) {
25503
+ return sdkScreen ?? this.options.store.topScreenAt(ts);
25504
+ }
25268
25505
  onFrames(message) {
25269
25506
  if (message.droppedFrames) {
25270
25507
  this.droppedFrames += message.droppedFrames;
@@ -25330,6 +25567,9 @@ var SdkBridge = class {
25330
25567
  this.frames.reset();
25331
25568
  if (this.current.framesActive) this.options.setGfxinfoPaused(false);
25332
25569
  }
25570
+ get sdkOptions() {
25571
+ return this.current.options ?? DEFAULT_SDK_OPTIONS;
25572
+ }
25333
25573
  update(changes) {
25334
25574
  this.current = { ...this.current, ...changes };
25335
25575
  this.options.onStatusChange();
@@ -25857,6 +26097,10 @@ var ProfilingSession = class {
25857
26097
  snapshot() {
25858
26098
  return this.store.snapshot();
25859
26099
  }
26100
+ /** Change this session's SDK options (partial); the session is re-announced and live links get `config`. */
26101
+ setSdkOptions(changes) {
26102
+ this.sdk.setOptions(changes);
26103
+ }
25860
26104
  /** Hand an SDK connection for this app to the session; it goes live once its PID checks out. */
25861
26105
  attachSdk(hello, link) {
25862
26106
  return this.sdk.attach(hello, link);
@@ -26119,6 +26363,13 @@ var SessionManager = class {
26119
26363
  }
26120
26364
  return { ok: true, handler: session.attachSdk(hello, link) };
26121
26365
  }
26366
+ /** Change the SDK options of the running session (announced with a `session` broadcast). false: no session. */
26367
+ setSdkOptions(changes) {
26368
+ const session = this.session;
26369
+ if (!session || session.isStopped) return false;
26370
+ session.setSdkOptions(changes);
26371
+ return true;
26372
+ }
26122
26373
  /** Poll `adb devices -l` every devicePollMs until close(). */
26123
26374
  startDevicePolling() {
26124
26375
  const poll = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lionscope",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight Android profiler with a web dashboard: CPU, memory, frames, screens and logcat for one app over adb",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- @font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2)format("woff2"),url(/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff)format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2)format("woff2"),url(/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff)format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2)format("woff2"),url(/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff)format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2)format("woff2"),url(/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff)format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2)format("woff2"),url(/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff)format("woff")}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--font-sans:"IBM Plex Sans", ui-sans-serif, -apple-system, "Segoe UI", sans-serif;--font-mono:"IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;--text-overline:.65625rem;--text-meta:.71875rem;--text-small:.78125rem;--text-body:.84375rem;--text-title:1rem;--text-readout:1.625rem;--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:24px;--space-6:32px;--radius-s:3px;--radius-m:5px;--radius-pill:999px;--ease-out:cubic-bezier(.16, 1, .3, 1);--dur-fast:.12s;--dur:.22s;--plane:#eceef1;--surface:#fbfbfc;--surface-sunk:#f3f4f6;--surface-raised:#fff;--hover-wash:#1114180b;--active-wash:#11141814;--ink:#111418;--ink-2:#474e5a;--ink-muted:#5f6673;--ink-inverse:#fbfbfc;--rule:#e2e4e8;--rule-strong:#c4c8cf;--focus:#2a6fdb;--brand:#d7262f;--event:#6250d6;--event-wash:#6250d61a;--jank:#c26a00;--jank-text:#9a5200;--jank-wash:#c26a001a;--good:#0b7d32;--good-wash:#0b7d321a;--critical:#c42f2f;--critical-wash:#c42f2f14;--viz-cpu:#2a78d6;--viz-fps:#2a78d6;--viz-frametime:#2a78d6;--viz-framemax:#8b919c;--viz-mem-native:#1baf7a;--viz-mem-java:#2a78d6;--viz-mem-graphics:#e87ba4;--viz-mem-code:#008300;--viz-mem-other:#8b919c;--viz-mem-total:#111418;--viz-threshold:#5f6673;--viz-stage-input:#1c5cab;--viz-stage-animation:#2a78d6;--viz-stage-layout:#5598e7;--viz-stage-draw:#86b6ef;--viz-stage-sync:#4fb94a;--viz-stage-command:#2b9927;--viz-stage-swap:#007a00;--viz-stage-delay:#babfc7;--viz-stage-gpu:#e87ba4;--viz-screen-1:#2a78d6;--viz-screen-2:#e87ba4;--viz-screen-3:#008300;--viz-screen-wash:.06;--log-v:#5f6673;--log-d:#3f4752;--log-i:#1f6fbf;--log-w:#8f5a00;--log-e:#c42f2f;--log-f:#a3122b;--shadow-pop:0 1px 0 #1114180a, 0 14px 36px -10px #11141838}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--plane:#0b0d10;--surface:#13161b;--surface-sunk:#0f1216;--surface-raised:#1a1e24;--hover-wash:#e9ebee0d;--active-wash:#e9ebee17;--ink:#e9ebee;--ink-2:#b1b7c1;--ink-muted:#8a919c;--ink-inverse:#0b0d10;--rule:#232831;--rule-strong:#343a45;--focus:#6b9cf0;--brand:#f0434b;--event:#9085e9;--event-wash:#9085e924;--jank:#f0a420;--jank-text:#f3b23a;--jank-wash:#f0a4201f;--good:#3cc26a;--good-wash:#3cc26a1f;--critical:#f07070;--critical-wash:#f070701a;--viz-cpu:#3987e5;--viz-fps:#3987e5;--viz-frametime:#3987e5;--viz-framemax:#6f7682;--viz-mem-native:#199e70;--viz-mem-java:#3987e5;--viz-mem-graphics:#d55181;--viz-mem-code:#008300;--viz-mem-other:#6f7682;--viz-mem-total:#e9ebee;--viz-threshold:#8a919c;--viz-stage-input:#256abf;--viz-stage-animation:#3987e5;--viz-stage-layout:#6da7ec;--viz-stage-draw:#9ec5f4;--viz-stage-sync:#56c050;--viz-stage-command:#33a02e;--viz-stage-swap:green;--viz-stage-delay:#4a505b;--viz-stage-gpu:#d55181;--viz-screen-1:#3987e5;--viz-screen-2:#d55181;--viz-screen-3:#008300;--viz-screen-wash:.085;--log-v:#8a919c;--log-d:#c9ced6;--log-i:#6aa8f0;--log-w:#e8b04a;--log-e:#f07070;--log-f:#ff6b81;--shadow-pop:0 0 0 1px #e9ebee0f, 0 18px 40px -12px #000000b3}}*,:before,:after{box-sizing:border-box}html,body{margin:0;padding:0}body{background:var(--plane);color:var(--ink);font-family:var(--font-sans);font-size:var(--text-body);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizelegibility;line-height:1.45}#root{min-height:100dvh}button,input,select{font:inherit;color:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed}:focus-visible{outline:2px solid var(--focus);outline-offset:2px}code,kbd{font-family:var(--font-mono);font-size:.95em}.sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.overline{font-size:var(--text-overline);letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted);font-weight:600}.mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums}.btn{justify-content:center;align-items:center;gap:var(--space-2);height:30px;padding:0 var(--space-3);border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface);font-size:var(--text-small);white-space:nowrap;transition:background-color var(--dur-fast) linear, border-color var(--dur-fast) linear, transform var(--dur-fast) var(--ease-out);font-weight:500;display:inline-flex}.btn:hover:not(:disabled){background:var(--hover-wash);border-color:var(--ink-muted)}.btn:active:not(:disabled){background:var(--active-wash);transform:translateY(1px)}.btn:disabled{opacity:.45}.btn--primary{background:var(--ink);border-color:var(--ink);color:var(--ink-inverse)}.btn--primary:hover:not(:disabled){background:var(--ink-2);border-color:var(--ink-2)}.btn--icon{width:30px;padding:0}.btn svg{flex:none}.segmented{border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface);display:inline-flex;overflow:hidden}.segmented>button{min-width:34px;height:26px;padding:0 var(--space-2);border:0;border-left:1px solid var(--rule);font-family:var(--font-mono);font-size:var(--text-meta);color:var(--ink-2);transition:background-color var(--dur-fast) linear, color var(--dur-fast) linear;background:0 0;font-weight:500}.segmented>button:first-child{border-left:0}.segmented>button:hover:not([aria-checked=true]):not([aria-pressed=true]){background:var(--hover-wash);color:var(--ink)}.segmented>button[aria-checked=true],.segmented>button[aria-pressed=true]{background:var(--ink);color:var(--ink-inverse)}.segmented>button:focus-visible{outline-offset:-2px}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}.logcat{background:var(--surface);flex-direction:column;min-width:0;min-height:0;display:flex;container:logcat/inline-size}.logcat-head{gap:var(--space-2);padding:var(--space-3) var(--space-4) var(--space-3);border-bottom:1px solid var(--rule);display:grid}.logcat-title-row{align-items:baseline;gap:var(--space-2);display:flex}.logcat-title{font-size:var(--text-small);letter-spacing:.01em;margin:0;font-weight:600}.logcat-pid{color:var(--ink-muted);font-size:var(--text-meta)}.logcat-count{color:var(--ink-2);font-size:var(--text-meta);margin-left:auto}.logcat-count-unit{color:var(--ink-muted)}.logcat-clear{height:24px;padding:0 var(--space-2);font-size:var(--text-meta);align-self:center}.logcat-filters{gap:var(--space-2);flex-wrap:wrap;display:flex}.level-filter>button:not([data-included]){color:var(--ink-muted);text-decoration:line-through;-webkit-text-decoration-color:var(--rule-strong);text-decoration-color:var(--rule-strong)}.level-filter>button[data-included]:not([aria-checked=true]){background:var(--surface-sunk)}.logcat-search{min-width:0;color:var(--ink-muted);flex:160px;align-items:center;display:flex;position:relative}.logcat-search svg{pointer-events:none;position:absolute;left:9px}.logcat-search input{width:100%;height:28px;padding:0 var(--space-2) 0 28px;border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface-sunk);color:var(--ink);font-family:var(--font-mono);font-size:var(--text-meta);transition:border-color var(--dur-fast) linear, background-color var(--dur-fast) linear}.logcat-search input::placeholder{color:var(--ink-muted)}.logcat-search input:hover{border-color:var(--ink-muted)}.logcat-search input:focus-visible{border-color:var(--focus);background:var(--surface);outline-offset:0}.logcat-body{flex-direction:column;flex:auto;min-height:0;display:flex;position:relative}.logcat-list{min-height:0;padding:var(--space-1) 0;overscroll-behavior:contain;font-family:var(--font-mono);font-size:var(--text-meta);scrollbar-color:var(--rule-strong) transparent;flex:auto;margin:0;line-height:1.5;list-style:none;overflow-y:auto}.log-row{padding:1px var(--space-4) 1px calc(var(--space-4) - 2px);border-left:2px solid #0000;grid-template-columns:12.5ch 1.5ch minmax(0,15ch) minmax(0,1fr);column-gap:1.25ch;display:grid}.log-row:hover{background:var(--hover-wash)}.log-time{color:var(--ink-muted);font-variant-numeric:tabular-nums;white-space:nowrap}.log-level{text-align:center;font-weight:600}.log-tag{color:var(--ink-2);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.log-msg{color:var(--ink);white-space:pre-wrap;overflow-wrap:anywhere}.log-row mark{background:var(--event-wash);box-shadow:0 0 0 1px var(--event-wash);color:inherit;border-radius:2px}.log-row[data-level=V] .log-level{color:var(--log-v)}.log-row[data-level=V] .log-msg{color:var(--ink-2)}.log-row[data-level=D] .log-level{color:var(--log-d)}.log-row[data-level=I] .log-level{color:var(--log-i)}.log-row[data-level=W]{border-left-color:var(--jank);background:var(--jank-wash)}.log-row[data-level=W] .log-level{color:var(--log-w)}.log-row[data-level=E],.log-row[data-level=F]{border-left-color:var(--critical);background:var(--critical-wash)}.log-row[data-level=E] .log-level{color:var(--log-e)}.log-row[data-level=F] .log-level{color:var(--log-f)}.log-row[data-level=F] .log-msg{font-weight:500}.logcat-empty{padding:var(--space-5);color:var(--ink-muted);font-size:var(--text-small);text-align:center;flex:1;place-content:center;display:grid}.logcat-empty p{max-width:32ch;margin:0}.logcat-empty-title{color:var(--ink-2);font-weight:600;margin-bottom:var(--space-1)!important}.jump-latest{bottom:var(--space-3);height:28px;padding:0 var(--space-3);border:1px solid var(--ink);border-radius:var(--radius-pill);background:var(--ink);box-shadow:var(--shadow-pop);color:var(--ink-inverse);font-size:var(--text-meta);animation:jump-in var(--dur) var(--ease-out) both;align-items:center;gap:6px;font-weight:500;display:inline-flex;position:absolute;left:50%;transform:translate(-50%)}.jump-latest:hover{background:var(--ink-2)}.jump-count{border-left:1px solid color-mix(in srgb, var(--ink-inverse) 35%, transparent);padding-left:6px}@keyframes jump-in{0%{opacity:0;transform:translate(-50%,6px)}}@container logcat (width<=560px){.log-row{grid-template-columns:12.5ch 1.5ch minmax(0,1fr);padding-block:2px}.log-msg{grid-column:2/-1}}.screens{background:var(--surface);flex-direction:column;flex:auto;min-width:0;min-height:0;display:flex;container:screens/inline-size}.screens-head{justify-content:space-between;align-items:center;gap:var(--space-2);min-height:44px;padding:var(--space-2) var(--space-4);border-bottom:1px solid var(--rule);flex-wrap:wrap;display:flex}.screens-scope{align-items:baseline;gap:var(--space-2);color:var(--ink-2);font-size:var(--text-meta);margin:0;display:inline-flex}.screens-filter{max-width:100%;height:26px;padding:0 var(--space-2);border-color:var(--ink);font-size:var(--text-meta)}.screens-filter-name{text-overflow:ellipsis;font-weight:600;overflow:hidden}.screens-filter-hint{color:var(--ink-muted)}.screens-body{flex:auto;min-height:0;overflow:auto}.screens-table{border-collapse:collapse;width:100%;font-size:var(--text-small)}.screens-table thead th{z-index:1;border-bottom:1px solid var(--rule-strong);background:var(--surface);color:var(--ink-muted);font-size:var(--text-overline);letter-spacing:.06em;text-align:right;text-transform:uppercase;white-space:nowrap;padding:0;font-weight:600;position:sticky;top:0}.screens-table thead th[data-col=name],.screens-table thead th[data-col=dominant]{text-align:left}.screens-table thead th[data-col=dominant]>span{padding:8px var(--space-4) 7px var(--space-2);display:inline-block}.sort-btn{padding:8px var(--space-2) 7px;color:inherit;font:inherit;letter-spacing:inherit;text-transform:inherit;transition:color var(--dur-fast) linear;background:0 0;border:0;align-items:center;gap:4px;display:inline-flex}th[data-col=name] .sort-btn{padding-left:var(--space-4)}.sort-btn:hover{color:var(--ink)}.sort-btn svg{opacity:0;transition:opacity var(--dur-fast) linear, transform var(--dur) var(--ease-out)}th[aria-sort] .sort-btn{color:var(--ink)}th[aria-sort] .sort-btn svg{opacity:1}th[aria-sort=ascending] .sort-btn svg{transform:rotate(180deg)}.screens-table tbody tr{border-bottom:1px solid var(--rule);cursor:pointer;transition:background-color var(--dur-fast) linear}.screens-table tbody tr:hover{background:var(--hover-wash)}.screens-table tbody tr[data-selected]{background:var(--active-wash);box-shadow:inset 2px 0 0 var(--ink)}.screens-table tbody th,.screens-table td{padding:7px var(--space-2);text-align:right;vertical-align:middle;white-space:nowrap;font-weight:400}.screens-table tbody th{width:40%;max-width:0;padding-left:var(--space-4);text-align:left}.screens-table td.mono{font-size:var(--text-meta)}.screen-cell{max-width:100%;color:var(--ink);font:inherit;text-align:left;background:0 0;border:0;align-items:center;gap:7px;padding:0;font-weight:500;display:inline-flex}.screen-cell[data-none]{color:var(--ink-muted);font-family:var(--font-mono);font-size:var(--text-meta)}.screen-cell-name{text-overflow:ellipsis;overflow:hidden}.screen-cell:focus-visible{outline-offset:3px}td[data-col=jankPercent]{min-width:64px}.jank-value[data-flag]{color:var(--jank-text);font-weight:600}.jank-meter{background:var(--rule);border-radius:2px;height:3px;margin-top:3px;display:block;overflow:hidden}.jank-meter>span{background:var(--jank);transform-origin:0;height:100%;transition:transform var(--dur) var(--ease-out);border-radius:2px;display:block}.p90-worst{color:var(--ink-muted);font-size:var(--text-overline);margin-top:2px;display:block}td[data-col=dominant]{padding-right:var(--space-4);text-align:left}.dominant{font-size:var(--text-meta);align-items:center;gap:6px;display:inline-flex}.dominant-name{color:var(--ink-2)}.stage-profile{gap:var(--space-2);padding:var(--space-3) var(--space-4) var(--space-4);animation:profile-in var(--dur) var(--ease-out) both;margin:0;display:grid}@keyframes profile-in{0%{opacity:0;transform:translateY(3px)}}.stage-bar{gap:2px;height:10px;display:flex}.stage-seg{background:var(--key,var(--ink-muted));border-radius:2px;min-width:2px}.stage-list{font-size:var(--text-meta);gap:3px;margin:0;padding:0;list-style:none;display:grid}.stage-list li{align-items:center;gap:var(--space-2);grid-template-columns:auto 1fr auto 4ch;display:grid}.stage-name{color:var(--ink-2)}.stage-share{color:var(--ink-muted);text-align:right}.screens-note{padding:var(--space-3) var(--space-4);color:var(--ink-2);font-size:var(--text-small);margin:0}.screens-empty{gap:var(--space-2);max-width:560px;padding:var(--space-5) var(--space-4);color:var(--ink-2);font-size:var(--text-small);grid-template-columns:minmax(0,1fr);display:grid}.screens-empty p{margin:0}.screens-empty-title{color:var(--ink);font-size:var(--text-body);font-weight:600}.screens-empty[data-tone=critical] .screens-empty-title{color:var(--log-e)}.screens-empty-detail{padding:var(--space-2) var(--space-3);border-left:2px solid var(--critical);border-radius:var(--radius-s);background:var(--critical-wash);color:var(--ink);font-size:var(--text-meta);overflow-wrap:anywhere}.screens-empty .screens-empty-note{color:var(--ink-muted);font-size:var(--text-meta)}.screens-snippet{margin:var(--space-1) 0 0;padding:var(--space-2) var(--space-3);border:1px solid var(--rule);border-radius:var(--radius-s);background:var(--surface-sunk);color:var(--ink);font-size:var(--text-meta);line-height:1.55;overflow-x:auto}@container screens (width<=560px){.screens-table [data-col=slowPercent]{display:none}}@container screens (width<=440px){.screens-table [data-col=typicalP90Ms]{display:none}}.side{background:var(--surface);flex-direction:column;min-width:0;min-height:0;display:flex}.side-tabs{gap:var(--space-1);padding:0 var(--space-3);border-bottom:1px solid var(--rule);background:var(--surface);flex:none;display:flex}.side-tab{height:36px;padding:0 var(--space-2);color:var(--ink-muted);font-size:var(--text-small);letter-spacing:.01em;transition:color var(--dur-fast) linear;background:0 0;border:0;align-items:center;gap:6px;font-weight:600;display:inline-flex;position:relative}.side-tab:after{right:var(--space-2);bottom:-1px;left:var(--space-2);background:var(--ink);content:"";height:2px;transition:transform var(--dur) var(--ease-out);border-radius:1px;position:absolute;transform:scaleX(0)}.side-tab:hover,.side-tab[aria-selected=true]{color:var(--ink)}.side-tab[aria-selected=true]:after{transform:scaleX(1)}.side-tab:focus-visible{outline-offset:-2px}.side-tab-count{border-radius:var(--radius-pill);background:var(--surface-sunk);min-width:18px;box-shadow:inset 0 0 0 1px var(--rule-strong);color:var(--ink-2);font-size:var(--text-overline);text-align:center;padding:0 5px;font-weight:500;line-height:16px}.side-panel{flex-direction:column;flex:auto;min-width:0;min-height:0;display:flex}.side-panel:focus-visible{outline-offset:-2px}.side-panel[hidden]{display:none}.side-panel>.logcat{flex:auto}.stat-strip{border-bottom:1px solid var(--rule)}.readouts{grid-template-columns:repeat(6,minmax(0,1fr));margin:0;display:grid}.readout{min-width:0;padding:10px var(--space-4) 9px;border-right:1px solid var(--rule)}.readout:last-child{border-right:0}.readout .overline{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.readout-body{align-items:baseline;gap:2px var(--space-2);flex-wrap:wrap;margin:2px 0 0;display:flex}.readout-value{font-size:var(--text-readout);letter-spacing:-.02em;font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:500;line-height:1.05}.readout[data-empty] .readout-value{color:var(--ink-muted);font-weight:400}.readout-note{color:var(--ink-2);font-size:var(--text-body);white-space:nowrap;align-self:center;margin:5px 0 4px;font-weight:500}.readout[title]{cursor:help}.readout-unit{color:var(--ink-muted);font-size:var(--text-small);letter-spacing:0;margin-left:3px;font-weight:400}.readout-detail{text-overflow:ellipsis;color:var(--ink-muted);font-size:var(--text-meta);white-space:nowrap;flex-basis:100%;margin-top:1px;overflow:hidden}.readout-flag{border-radius:var(--radius-s);background:var(--jank-wash);height:18px;color:var(--jank-text);font-size:var(--text-overline);letter-spacing:.04em;text-transform:uppercase;white-space:nowrap;animation:flag-in var(--dur) var(--ease-out) both;align-items:center;gap:4px;padding:0 6px;font-weight:600;display:inline-flex}.readout-flag svg{color:var(--jank)}@keyframes flag-in{0%{opacity:0;transform:scale(.92)}}.cursor-readout{margin-right:var(--space-2);font-size:var(--text-small);white-space:nowrap;align-items:baseline;gap:6px;display:inline-flex}.live-btn{border-color:var(--ink)}.live-dot{background:var(--good);border-radius:50%;width:7px;height:7px}@media (width<=1400px){.readout{padding-inline:var(--space-3)}.readout-value{font-size:1.375rem}}@media (width<=760px){.readouts{grid-template-columns:repeat(3,minmax(0,1fr))}.readout{border-bottom:1px solid var(--rule)}.readout:nth-child(3n){border-right:0}.readout:nth-child(n+4){border-bottom:0}}@media (width<=420px){.readouts{grid-template-columns:repeat(2,minmax(0,1fr))}.readout:nth-child(3n){border-right:1px solid var(--rule)}.readout:nth-child(2n){border-right:0}.readout:nth-child(n+4){border-bottom:1px solid var(--rule)}.readout:nth-child(n+5){border-bottom:0}}.empty-state{max-width:440px;padding:var(--space-5) var(--space-5) var(--space-4);border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface);box-shadow:var(--shadow-pop);color:var(--ink-2);font-size:var(--text-body);position:relative}.empty-state:before{border-radius:var(--radius-m) 0 0 var(--radius-m);background:var(--ink);content:"";width:3px;position:absolute;inset:-1px auto -1px -1px}.empty-state[data-tone=critical]:before{background:var(--critical)}.empty-state p{margin:0 0 var(--space-2)}.empty-state .overline{margin-bottom:var(--space-1)}.empty-title{margin:0 0 var(--space-3);color:var(--ink);font-size:var(--text-title);letter-spacing:-.005em;font-weight:600}.empty-code{margin:var(--space-2) 0 var(--space-1);padding:var(--space-2) var(--space-3);border:1px solid var(--rule);border-radius:var(--radius-s);background:var(--surface-sunk);color:var(--ink);font-family:var(--font-mono);font-size:var(--text-small)}.empty-state code{border-radius:var(--radius-s);background:var(--surface-sunk);color:var(--ink);padding:1px 4px}.empty-steps{gap:var(--space-2);counter-reset:step;margin:0;padding:0;list-style:none;display:grid}.empty-steps li{gap:var(--space-2);counter-increment:step;grid-template-columns:20px 1fr;display:grid}.empty-steps li:before{color:var(--ink-muted);font-family:var(--font-mono);font-size:var(--text-small);content:counter(step, decimal-leading-zero)}.empty-devices{gap:var(--space-2);margin:0;padding:0;list-style:none;display:grid}.empty-devices li{padding-left:var(--space-3);border-left:2px solid var(--jank);gap:2px;display:grid}.empty-devices .mono{color:var(--ink);font-size:var(--text-small)}.empty-error{padding:var(--space-2) var(--space-3);border-radius:var(--radius-s);background:var(--critical-wash);color:var(--log-e);font-size:var(--text-small);overflow-wrap:anywhere}.timeline-overlay{background-color:color-mix(in srgb, var(--surface) 82%, transparent);background-image:radial-gradient(circle, var(--rule-strong) 1px, transparent 1.2px);background-size:16px 16px}.banner{align-items:center;gap:var(--space-1) var(--space-2);padding:7px var(--space-4);border-bottom:1px solid var(--rule);background:var(--surface);font-size:var(--text-small);animation:banner-in var(--dur) var(--ease-out) both;flex-wrap:wrap;display:flex}.banner[data-tone=critical]{border-bottom-color:color-mix(in srgb, var(--critical) 35%, var(--rule));background:color-mix(in srgb, var(--critical) 7%, var(--surface))}.banner[data-tone=quiet]{color:var(--ink-2)}.banner code{font-size:var(--text-meta)}.banner-meta{color:var(--ink-muted);font-size:var(--text-meta)}.banner-hint{color:var(--ink-2)}.banner-dot{background:var(--critical);border-radius:50%;width:8px;height:8px}.banner-spinner{border:1.5px solid var(--rule-strong);border-top-color:var(--ink);border-radius:50%;width:10px;height:10px;animation:.9s linear infinite spin}.banner-close{border-radius:var(--radius-s);width:24px;height:24px;color:var(--ink-2);background:0 0;border:0;place-items:center;margin-left:auto;display:grid}.banner-close:hover{background:var(--hover-wash);color:var(--ink)}@keyframes banner-in{0%{opacity:0;transform:translateY(-4px)}}@keyframes spin{to{transform:rotate(360deg)}}.crash{min-height:100dvh;padding:var(--space-5) var(--space-4);place-items:center;display:grid}.crash-actions{gap:var(--space-2);margin-top:var(--space-3);display:flex}.uplot,.uplot *,.uplot :before,.uplot :after{box-sizing:border-box}.uplot{width:min-content;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}.u-title{text-align:center;font-size:18px;font-weight:700}.u-wrap{-webkit-user-select:none;user-select:none;position:relative}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{width:100%;height:100%;display:block;position:relative}.u-axis{position:absolute}.u-legend{text-align:center;margin:auto;font-size:14px}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{vertical-align:middle;display:inline-block}.u-legend .u-marker{width:1em;height:1em;margin-right:4px;background-clip:padding-box!important}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{pointer-events:none;background:#00000012;position:absolute}.u-cursor-x,.u-cursor-y{pointer-events:none;will-change:transform;position:absolute;top:0;left:0}.u-hz .u-cursor-x,.u-vt .u-cursor-y{border-right:1px dashed #607d8b;height:100%}.u-hz .u-cursor-y,.u-vt .u-cursor-x{border-bottom:1px dashed #607d8b;width:100%}.u-cursor-pt{pointer-events:none;will-change:transform;border:0 solid;border-radius:50%;position:absolute;top:0;left:0;background-clip:padding-box!important}.u-axis.u-off,.u-select.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-cursor-pt.u-off{display:none}.lane.lane--screens{z-index:4;flex:none;height:auto;min-height:0;position:relative}.lane.lane--screens .lane-head{padding-bottom:2px}.screen-readout{min-width:0;font-size:var(--text-meta);white-space:nowrap;align-items:center;gap:6px;margin:0;display:inline-flex}.screen-readout-name{color:var(--ink);text-overflow:ellipsis;font-weight:500;overflow:hidden}.screen-readout[data-at-cursor] .legend-value{background:var(--hover-wash)}.screen-swatch{background:var(--band,var(--ink-muted));border-radius:2px;flex:none;width:9px;height:9px;display:inline-block}.screen-swatch[data-bg]{background:repeating-linear-gradient(135deg, var(--ink-muted) 0 1px, transparent 1px 3px);box-shadow:inset 0 0 0 1px var(--rule-strong)}[data-color=screen1]{--band:var(--viz-screen-1)}[data-color=screen2]{--band:var(--viz-screen-2)}[data-color=screen3]{--band:var(--viz-screen-3)}.screen-clear{max-width:220px;font-family:var(--font-sans);text-overflow:ellipsis;white-space:nowrap;align-items:center;gap:5px;display:inline-flex;overflow:hidden}.screen-clear-name{text-overflow:ellipsis;min-width:0;overflow:hidden}.screen-clear svg{opacity:.7;flex:none}.screen-view-link{border-radius:var(--radius-s);color:var(--ink-2);font-size:var(--text-meta);text-decoration:underline;-webkit-text-decoration-color:var(--rule-strong);text-decoration-color:var(--rule-strong);text-underline-offset:3px;white-space:nowrap;transition:color var(--dur-fast) linear, text-decoration-color var(--dur-fast) linear;background:0 0;border:0;align-self:center;align-items:center;gap:4px;padding:0 2px;font-weight:500;display:inline-flex}.screen-view-link:hover{color:var(--ink);-webkit-text-decoration-color:var(--ink);text-decoration-color:var(--ink)}.screen-view-link svg{transition:transform var(--dur) var(--ease-out)}.screen-view-link:hover svg{transform:translate(2px)}.screen-track-host{height:30px;position:relative}.screen-track{position:absolute;top:3px;bottom:5px}.screen-track:before{border-radius:var(--radius-s);background:var(--surface-sunk);content:"";position:absolute;inset:0}.screen-track-empty{padding-left:var(--space-2);color:var(--ink-muted);font-size:var(--text-meta);align-items:center;display:flex;position:absolute;inset:0}.screen-band{border:0;border-right:2px solid var(--surface);background:color-mix(in srgb, var(--band,var(--ink-muted)) 17%, var(--surface));min-width:3px;box-shadow:inset 2px 0 0 var(--band,var(--ink-muted));color:var(--ink);font-family:var(--font-sans);font-size:var(--text-meta);text-align:left;white-space:nowrap;transition:background-color var(--dur-fast) linear, opacity var(--dur-fast) linear;background-clip:padding-box;border-radius:2px;align-items:center;margin:0;padding:0;font-weight:500;line-height:1;display:flex;position:absolute;top:0;bottom:0;overflow:hidden}.screen-band[data-labelled]{padding:0 6px 0 7px}.screen-band-label{text-overflow:ellipsis;min-width:0;overflow:hidden}.screen-band[data-cluster]{background:repeating-linear-gradient(90deg, var(--ink-muted) 0 1px, transparent 1px 3px);box-shadow:none;opacity:.55;background-clip:padding-box}button.screen-band:hover{background-color:color-mix(in srgb, var(--band) 28%, var(--surface))}button.screen-band:focus-visible{z-index:2;outline-offset:0}.screen-band[data-open]{border-right:0;border-radius:2px 0 0 2px}.screen-band[data-selected]{z-index:1;background-color:color-mix(in srgb, var(--band) 34%, var(--surface));box-shadow:inset 3px 0 0 var(--band), inset 0 0 0 1px var(--band);font-weight:600}.screen-track[data-has-selection] .screen-band:not([data-selected]){opacity:.5}.screen-track[data-has-selection] .screen-band:not([data-selected]):hover{opacity:.85}.screen-band[data-bg]{background:repeating-linear-gradient(135deg, var(--rule-strong) 0 1px, color-mix(in srgb, var(--surface-sunk) 70%, var(--surface)) 1px 6px);box-shadow:none;color:var(--ink-muted);font-family:var(--font-mono);font-size:var(--text-overline);letter-spacing:.04em;text-transform:uppercase;background-clip:padding-box;font-weight:500}.screen-track-cursor,.screen-track-probe{z-index:3;opacity:0;pointer-events:none;width:0;position:absolute;top:-3px;bottom:-5px;left:0}.screen-track-cursor{border-left:1px solid var(--ink-muted)}.screen-track-probe{border-left:1px solid var(--ink);transition:opacity var(--dur-fast) linear}.screen-tip{z-index:20;border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface-raised);width:260px;box-shadow:var(--shadow-pop);pointer-events:none;animation:tip-in .14s var(--ease-out) both;padding:8px 10px 9px;position:absolute;top:calc(100% + 8px);transform:translate(-50%)}@keyframes tip-in{0%{opacity:0;transform:translate(-50%,-2px)}}.screen-tip p{margin:0}.screen-tip-title{align-items:center;gap:6px;min-width:0;display:flex}.screen-tip-name{font-size:var(--text-small);text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}.screen-tip-kind{border:1px solid var(--rule-strong);border-radius:var(--radius-s);color:var(--ink-2);font-size:var(--text-overline);letter-spacing:.04em;text-transform:uppercase;flex:none;margin-left:auto;padding:1px 5px;font-weight:600}.screen-tip .screen-tip-class{color:var(--ink-2);font-size:var(--text-meta);overflow-wrap:anywhere;margin-top:4px}.screen-tip .screen-tip-time{justify-content:space-between;gap:var(--space-2);border-top:1px solid var(--rule);color:var(--ink);font-size:var(--text-meta);margin-top:6px;padding-top:6px;display:flex}.screen-tip-time span{color:var(--ink-muted)}@media (forced-colors:active){.screen-band{border:1px solid canvastext}.screen-band[data-selected]{outline:2px solid highlight}}.timeline{flex-direction:column;flex:auto;min-height:0;display:flex;position:relative}.timeline-toolbar{z-index:3;align-items:center;gap:var(--space-2);min-height:40px;padding:5px var(--space-4);border-bottom:1px solid var(--rule);background:var(--surface);flex-wrap:wrap;display:flex;position:relative}.timeline-toolbar>.overline{margin-right:auto}.timeline-lanes{flex-direction:column;flex:auto;min-height:0;display:flex;overflow-y:auto}.timeline-lanes[data-dense] .lane:not(.lane--screens){min-height:108px}.timeline-lanes[data-dense] .lane-plot{min-height:68px}.lane{border-top:1px solid var(--rule);min-height:136px;animation:lane-in .42s var(--ease-out) both;flex-direction:column;flex:1 1 0;margin:0;display:flex}.lane:first-child{border-top:0}.lane:nth-child(2){animation-delay:40ms}.lane:nth-child(3){animation-delay:80ms}.lane:nth-child(4){animation-delay:.12s}.lane:nth-child(n+5){animation-delay:.16s}@keyframes lane-in{0%{opacity:0;transform:translateY(4px)}}.lane-head{justify-content:space-between;align-items:baseline;gap:var(--space-1) var(--space-4);padding:var(--space-2) var(--space-4) 0;flex-wrap:wrap;display:flex}.lane-name{align-items:baseline;gap:var(--space-2);min-width:0;display:flex}.lane-title{font-size:var(--text-small);letter-spacing:.01em;margin:0;font-weight:600}.lane-unit{color:var(--ink-muted);font-size:var(--text-meta);white-space:nowrap}.lane-hint{width:18px;height:18px;color:var(--ink-muted);cursor:help;border-radius:50%;align-self:center;place-items:center;display:inline-grid}.lane-hint:hover,.lane-hint:focus-visible{color:var(--ink);background:var(--hover-wash)}.lane-toggle{border:1px solid var(--rule-strong);border-radius:var(--radius-s);height:20px;color:var(--ink-2);font-family:var(--font-mono);font-size:var(--text-overline);transition:background-color var(--dur-fast) linear, color var(--dur-fast) linear;background:0 0;align-self:center;padding:0 7px;font-weight:500}.lane-toggle:hover{background:var(--hover-wash);color:var(--ink)}.lane-toggle[aria-pressed=true]{border-color:var(--ink);background:var(--ink);color:var(--ink-inverse)}.lane-legend{gap:2px var(--space-3);flex-wrap:wrap;margin:0;padding:0;list-style:none;display:flex}.legend-item{font-size:var(--text-meta);white-space:nowrap;align-items:center;gap:6px;display:inline-flex}.legend-label{color:var(--ink-muted)}.legend-value{border-radius:var(--radius-s);min-width:3.5ch;color:var(--ink);font-size:var(--text-meta);text-align:right;transition:background-color var(--dur-fast) linear;padding:0 3px;font-weight:500}.lane-legend[data-at-cursor] .legend-value{background:var(--hover-wash)}.legend-key{background:var(--key,var(--ink));border-radius:1px;flex:none;width:14px;height:2px;display:inline-block}.legend-key[data-key=thin]{height:1px}.legend-key[data-key=area]{border-radius:2px;width:9px;height:9px}.legend-key[data-key=bar]{border-radius:2px 2px 0 0;width:4px;height:10px}.legend-key[data-tint]{background:color-mix(in srgb, var(--key,var(--ink)) 45%, var(--surface))}.legend-key[data-color=cpu]{--key:var(--viz-cpu)}.legend-key[data-color=fps]{--key:var(--viz-fps)}.legend-key[data-color=jank]{--key:var(--jank)}.legend-key[data-color=frameTime]{--key:var(--viz-frametime)}.legend-key[data-color=frameMax]{--key:var(--viz-framemax)}.legend-key[data-color=memNative]{--key:var(--viz-mem-native)}.legend-key[data-color=memJava]{--key:var(--viz-mem-java)}.legend-key[data-color=memGraphics]{--key:var(--viz-mem-graphics)}.legend-key[data-color=memCode]{--key:var(--viz-mem-code)}.legend-key[data-color=memOther]{--key:var(--viz-mem-other)}.legend-key[data-color=memTotal]{--key:var(--viz-mem-total)}.legend-key[data-color=ink]{--key:var(--ink)}[data-color=stageInput]{--key:var(--viz-stage-input)}[data-color=stageAnimation]{--key:var(--viz-stage-animation)}[data-color=stageLayout]{--key:var(--viz-stage-layout)}[data-color=stageDraw]{--key:var(--viz-stage-draw)}[data-color=stageSync]{--key:var(--viz-stage-sync)}[data-color=stageCommand]{--key:var(--viz-stage-command)}[data-color=stageSwap]{--key:var(--viz-stage-swap)}[data-color=stageDelay]{--key:var(--viz-stage-delay)}[data-color=stageGpu]{--key:var(--viz-stage-gpu)}.legend-item[data-emphasis] .legend-label{color:var(--ink);font-weight:600}.legend-item[data-emphasis] .legend-value{box-shadow:inset 0 0 0 1px var(--rule-strong);font-weight:600}.legend-item[title]{cursor:help}.source-tag{border:1px solid var(--rule-strong);border-radius:var(--radius-s);color:var(--ink-2);font-family:var(--font-mono);font-size:var(--text-overline);white-space:nowrap;align-self:center;padding:1px 6px;font-weight:500}.source-tag[data-source=sdk]{border-color:color-mix(in srgb, var(--good) 45%, var(--rule-strong));background:var(--good-wash);color:var(--ink)}.source-tag[data-idle]{color:var(--ink-2);cursor:help;background:0 0;border-style:dashed}.lane-plot{flex:auto;min-height:92px;position:relative;overflow:hidden}.lane-plot>.uplot{width:100%;font-family:var(--font-mono);position:absolute;inset:0}.lane-plot .u-hz .u-cursor-x{border-right:1px solid var(--ink-muted)}.lane-plot .u-cursor-pt{box-shadow:0 0 0 1px var(--rule-strong)}.lane-probe{border-left:1px solid var(--ink);opacity:0;pointer-events:none;width:0;transition:opacity var(--dur-fast) linear;position:absolute;top:0;bottom:0;left:0}.lane-probe:before{background:var(--ink);width:7px;height:7px;box-shadow:0 0 0 2px var(--surface);content:"";border-radius:50%;position:absolute;top:0;left:-4px}.timeline-notice{align-items:center;gap:var(--space-2);padding:6px var(--space-4);border-bottom:1px solid var(--rule);background:var(--surface-sunk);color:var(--ink-2);font-size:var(--text-small);display:flex}.timeline-overlay{z-index:2;padding:var(--space-5);animation:overlay-in var(--dur) var(--ease-out) both;place-items:center;display:grid;position:absolute;inset:0}@keyframes overlay-in{0%{opacity:0}}@media (width<=1024px){.lane{flex:none;height:210px}}@media (width<=640px){.lane{height:184px}.lane-head{padding-inline:var(--space-3)}}.topbar{z-index:10;align-items:flex-end;gap:var(--space-5);padding:10px var(--space-4) 10px;border-bottom:1px solid var(--rule-strong);background:var(--plane);display:flex;position:relative}.topbar-brand{flex-direction:column;flex:none;align-self:center;gap:1px;display:flex}.brand{color:var(--ink);align-items:center;gap:8px;display:inline-flex}.brand-mark{flex:none}.brand-dot{fill:var(--brand)}.brand-word{font-size:var(--text-title);letter-spacing:-.015em;font-weight:600;line-height:1}.brand-word span{color:var(--ink-2);font-weight:400}.topbar-version{color:var(--ink-muted);font-size:var(--text-overline);letter-spacing:.02em;padding-left:30px}.topbar-controls{align-items:flex-end;gap:var(--space-2) var(--space-3);min-width:0;padding-left:var(--space-5);border-left:1px solid var(--rule-strong);flex-wrap:wrap;flex:auto;display:flex}.field{gap:3px;min-width:0;display:grid}.field--grow{flex:260px;max-width:440px}.field-row{gap:var(--space-1);display:flex}.select-wrap,.combo{min-width:0;position:relative}.select-wrap{flex:auto}.select,.combo-input{border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface);width:100%;height:30px;color:var(--ink);font-size:var(--text-small);text-overflow:ellipsis;transition:border-color var(--dur-fast) linear;padding:0 28px 0 10px}.select{appearance:none;cursor:pointer;width:300px;max-width:100%}.select:hover:not(:disabled),.combo-input:hover:not(:disabled){border-color:var(--ink-muted)}.select:disabled,.combo-input:disabled{background:var(--surface-sunk);color:var(--ink-2);cursor:not-allowed}.combo-input::placeholder{color:var(--ink-muted);font-family:var(--font-sans)}.combo[data-open] .combo-input{border-color:var(--focus);outline:none}.select-chevron{color:var(--ink-muted);pointer-events:none;transition:transform var(--dur) var(--ease-out);position:absolute;top:50%;right:10px;transform:translateY(-50%)}.combo[data-open] .select-chevron{transform:translateY(-50%)rotate(180deg)}.combo-pop{z-index:30;border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface-raised);width:max(100%,380px);max-width:calc(100vw - 32px);max-height:min(420px,60vh);box-shadow:var(--shadow-pop);transform-origin:0 0;animation:pop-in .16s var(--ease-out) both;flex-direction:column;display:flex;position:absolute;top:calc(100% + 4px);left:0}@keyframes pop-in{0%{opacity:0;transform:translateY(-3px)scale(.99)}}.combo-toolbar{justify-content:space-between;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--rule);display:flex}.check{font-size:var(--text-small);cursor:pointer;align-items:center;gap:6px;display:inline-flex}.check input{width:14px;height:14px;accent-color:var(--ink);margin:0}.combo-status{color:var(--ink-muted);font-size:var(--text-meta);white-space:nowrap}.combo-list{padding:var(--space-1);flex:auto;margin:0;list-style:none;overflow-y:auto}.combo-option{align-items:center;gap:var(--space-2);min-height:30px;padding:0 var(--space-2);border-radius:var(--radius-s);cursor:pointer;display:flex}.combo-option[aria-selected=true]{background:var(--active-wash)}.combo-option[data-current]{box-shadow:inset 2px 0 0 var(--ink)}.combo-option[data-kind=reveal]{color:var(--ink-2);font-size:var(--text-small)}.combo-option[data-kind=reveal] .combo-name:before{color:var(--ink-muted);content:"+";margin-right:6px}.combo-note{padding:6px var(--space-3);border-bottom:1px solid var(--rule);background:var(--surface-sunk);color:var(--ink-2);font-size:var(--text-meta);margin:0}.combo-note--foot{border-top:1px solid var(--rule);border-bottom:0}.combo-kbd{border:1px solid var(--rule-strong);border-radius:var(--radius-s);color:var(--ink-muted);font-size:var(--text-overline);padding:0 4px}.combo-name{min-width:0;font-size:var(--text-small);text-overflow:ellipsis;white-space:nowrap;flex:auto;overflow:hidden}.combo-name mark{background:var(--event-wash);color:inherit;border-radius:2px}.combo-empty{padding:var(--space-3) var(--space-2);color:var(--ink-muted);font-size:var(--text-small)}.pid-badge{border-radius:var(--radius-s);background:var(--good-wash);color:var(--good);font-size:var(--text-overline);flex:none;padding:1px 6px;font-weight:600}.sys-badge{color:var(--ink-muted);font-size:var(--text-overline);letter-spacing:.04em;text-transform:uppercase;flex:none}.topbar-session{align-items:center;gap:var(--space-3);min-width:0;display:flex}.session-btn{min-width:84px}.session-btn--stop{border-color:var(--ink);font-weight:600}.status-pill{border:1px solid var(--rule-strong);border-radius:var(--radius-pill);background:var(--surface);min-width:0;max-width:360px;height:26px;color:var(--ink-2);font-size:var(--text-small);white-space:nowrap;align-items:center;gap:6px;padding:0 10px;font-weight:500;display:inline-flex}.status-dot{background:var(--ink-muted);border-radius:50%;flex:none;width:7px;height:7px;position:relative}.status-meta{color:var(--ink-muted);font-size:var(--text-meta);font-weight:400}.status-meta:before{color:var(--rule-strong);content:"·";margin-right:6px}.status-error{font-family:var(--font-sans);text-overflow:ellipsis;overflow:hidden}.status-pill[data-status=running]{border-color:color-mix(in srgb, var(--good) 45%, var(--rule-strong));color:var(--ink)}.status-pill[data-status=running] .status-dot{background:var(--good)}.status-pill[data-status=running] .status-dot:after,.status-pill[data-status=waiting] .status-dot:after{content:"";animation:pulse 1.6s var(--ease-out) infinite;border:1.5px solid;border-radius:50%;position:absolute;inset:-3px}.status-pill[data-status=running] .status-dot:after{color:var(--good)}.status-pill[data-status=waiting]{color:var(--ink)}.status-pill[data-status=waiting] .status-dot{background:var(--ink-2)}.status-pill[data-status=waiting] .status-dot:after{color:var(--ink-2)}.status-pill[data-status=error]{border-color:color-mix(in srgb, var(--critical) 50%, var(--rule-strong));background:var(--critical-wash);color:var(--log-e)}@keyframes pulse{0%{opacity:.9;transform:scale(.6)}to{opacity:0;transform:scale(1.6)}}@media (prefers-reduced-motion:reduce){.status-dot:after{display:none}}.sdk-status{position:relative}.sdk-pill{border:1px solid var(--rule-strong);border-radius:var(--radius-pill);background:var(--surface);height:26px;color:var(--ink-2);font-size:var(--text-small);white-space:nowrap;align-items:center;gap:6px;padding:0 10px;font-weight:500;display:inline-flex}.sdk-dot{background:var(--ink-muted);border-radius:2px;flex:none;width:7px;height:7px}.sdk-pill[data-state=connected]{color:var(--ink)}.sdk-pill[data-state=connected] .sdk-dot{background:var(--good)}.sdk-meta{color:var(--ink-muted);font-size:var(--text-meta);font-weight:400}.sdk-meta:before{color:var(--rule-strong);content:"·";margin:0 5px}.sdk-pill[data-state=disconnected] .sdk-dot{box-shadow:inset 0 0 0 1.5px var(--ink-muted);background:0 0}button.sdk-pill{transition:background-color var(--dur-fast) linear, border-color var(--dur-fast) linear;border-style:dashed}button.sdk-pill:hover,button.sdk-pill[aria-expanded=true]{border-color:var(--ink-muted);background:var(--hover-wash);color:var(--ink)}button.sdk-pill .sdk-dot{box-shadow:inset 0 0 0 1.5px var(--ink-muted);background:0 0}.sdk-pop{z-index:30;gap:var(--space-2);width:540px;max-width:calc(100vw - 32px);padding:var(--space-3);border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface-raised);box-shadow:var(--shadow-pop);transform-origin:100% 0;animation:pop-in .16s var(--ease-out) both;grid-template-columns:minmax(0,1fr);display:grid;position:absolute;top:calc(100% + 6px);right:0}@media (width<=640px){.sdk-pop{transform-origin:0 0;left:0;right:auto}}button.sdk-pill[data-state=failed]{border-style:solid;border-color:color-mix(in srgb, var(--critical) 50%, var(--rule-strong));background:var(--critical-wash);color:var(--log-e)}button.sdk-pill[data-state=failed] .sdk-dot{background:var(--critical);box-shadow:none}.sdk-pop:focus-visible{outline-offset:0}.sdk-pop-error{padding:var(--space-2) var(--space-3);border-left:2px solid var(--critical);border-radius:var(--radius-s);background:var(--critical-wash);color:var(--ink);font-size:var(--text-meta);overflow-wrap:anywhere;margin:0}.sdk-pop-head{justify-content:space-between;align-items:center;gap:var(--space-2);display:flex}.sdk-pop-title{font-size:var(--text-small);margin:0;font-weight:600}.sdk-pop-close{border-color:#0000;width:24px;height:24px}.sdk-pop-text{color:var(--ink-2);font-size:var(--text-meta);margin:0;line-height:1.5}.sdk-snippet{border:1px solid var(--rule);border-radius:var(--radius-s);background:var(--surface-sunk);position:relative}.sdk-snippet pre{padding:var(--space-2) var(--space-3);color:var(--ink);font-size:var(--text-meta);margin:0;line-height:1.55;overflow-x:auto}.sdk-snippet-bar{justify-content:space-between;align-items:center;gap:var(--space-2);padding:4px 4px 4px var(--space-3);border-bottom:1px solid var(--rule);display:flex}.sdk-copy{height:24px;padding:0 var(--space-2);font-size:var(--text-meta)}.topbar-link{color:var(--ink-muted);font-size:var(--text-meta);flex:none;align-self:center;align-items:center;gap:6px;margin-left:auto;display:inline-flex}.link-dot{background:var(--good);border-radius:50%;width:6px;height:6px}.topbar-link:not([data-state=open]) .link-dot{background:var(--critical)}@media (width<=1180px){.topbar{align-items:center;gap:var(--space-2) var(--space-4);flex-wrap:wrap}.topbar-controls{border-left:0;flex-basis:100%;order:3;padding-left:0}}@media (width<=640px){.field,.field--grow{flex:100%;max-width:none}.select{width:100%}.topbar-session{flex-wrap:wrap;flex:100%}.status-pill{max-width:100%}}.app{flex-direction:column;min-width:0;height:100dvh;display:flex}.banners:empty{display:none}.workspace{background:var(--rule-strong);flex:auto;grid-template-columns:minmax(0,2fr) minmax(360px,1fr);gap:1px;min-height:0;display:grid}.instrument{background:var(--surface);flex-direction:column;min-width:0;min-height:0;display:flex}.timeline-notice{margin:0}@media (width<=1024px){.app{height:auto;min-height:100dvh}.workspace{grid-template-columns:minmax(0,1fr)}.side{height:min(72vh,680px)}}