logshield-cli 0.4.4 → 0.5.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +50 -3
  3. package/dist/cli/index.cjs +52 -18
  4. package/package.json +36 -8
  5. package/dist/android-chrome-192x192.png +0 -0
  6. package/dist/android-chrome-512x512.png +0 -0
  7. package/dist/apple-touch-icon.png +0 -0
  8. package/dist/assets/index-B3qxIuiz.css +0 -1
  9. package/dist/assets/index-DDJ1Wxio.js +0 -29
  10. package/dist/engine/applyRules.js +0 -27
  11. package/dist/engine/engine/applyRules.d.ts +0 -5
  12. package/dist/engine/engine/applyRules.js +0 -30
  13. package/dist/engine/engine/guard.d.ts +0 -1
  14. package/dist/engine/engine/guard.js +0 -12
  15. package/dist/engine/engine/sanitizeLog.d.ts +0 -11
  16. package/dist/engine/engine/sanitizeLog.js +0 -18
  17. package/dist/engine/guard.js +0 -9
  18. package/dist/engine/rules/cloud.d.ts +0 -2
  19. package/dist/engine/rules/cloud.js +0 -15
  20. package/dist/engine/rules/credentials.d.ts +0 -2
  21. package/dist/engine/rules/credentials.js +0 -15
  22. package/dist/engine/rules/creditCard.d.ts +0 -2
  23. package/dist/engine/rules/creditCard.js +0 -15
  24. package/dist/engine/rules/custom.d.ts +0 -2
  25. package/dist/engine/rules/custom.js +0 -14
  26. package/dist/engine/rules/index.d.ts +0 -11
  27. package/dist/engine/rules/index.js +0 -38
  28. package/dist/engine/rules/tokens.d.ts +0 -2
  29. package/dist/engine/rules/tokens.js +0 -20
  30. package/dist/engine/rules/types.d.ts +0 -9
  31. package/dist/engine/rules/types.js +0 -2
  32. package/dist/engine/rules/urls.d.ts +0 -2
  33. package/dist/engine/rules/urls.js +0 -10
  34. package/dist/engine/sanitizeLog.js +0 -15
  35. package/dist/engine/utils/luhn.d.ts +0 -1
  36. package/dist/engine/utils/luhn.js +0 -19
  37. package/dist/favicon-16x16.png +0 -0
  38. package/dist/favicon-32x32.png +0 -0
  39. package/dist/favicon.ico +0 -0
  40. package/dist/features.jsx +0 -462
  41. package/dist/robots.txt +0 -4
  42. package/dist/rules/cloud.js +0 -12
  43. package/dist/rules/credentials.js +0 -12
  44. package/dist/rules/creditCard.js +0 -12
  45. package/dist/rules/custom.js +0 -11
  46. package/dist/rules/index.js +0 -35
  47. package/dist/rules/tokens.js +0 -17
  48. package/dist/rules/types.js +0 -1
  49. package/dist/rules/urls.js +0 -7
  50. package/dist/site.webmanifest +0 -20
  51. package/dist/utils/luhn.js +0 -16
  52. package/dist/vite.svg +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.5.0
4
+
5
+ ### Security
6
+
7
+ - Hardened dry-run result shape: dry-run no longer returns the raw input in `output` (safe to serialize and avoids re-leakage in programmatic contexts)
8
+ - `--dry-run` can be combined with `--json` for machine-readable detection without leaking log content (`output` is intentionally empty)
9
+
10
+ ### Added
11
+
12
+ - Added detection-only helper `scanLog(input)` (internal only; safe to serialize)
13
+
14
+ ### Compatibility
15
+
16
+ - CLI behavior is unchanged: dry-run still prints a human report and never echoes log content
17
+ - Programmatic consumers: `dryRun` now returns `output: ""` (intentional)
18
+ - npm package ships CLI only; no supported JS API surface is published
19
+
3
20
  ## v0.4.4
4
21
 
5
22
  ### Fixed
@@ -18,7 +35,7 @@
18
35
 
19
36
  - Prevented API key redaction from corrupting header names (`x-api-key`)
20
37
  - Preserved key labels when redacting `api_key=...` values
21
- - Corrected CLI exit code for invalid flag combinations (`--json --dry-run` now exits with code 2)
38
+ - Corrected CLI exit code for invalid flag combinations (e.g., `--summary --json` exits with code 2)
22
39
 
23
40
  ### Improved
24
41
 
package/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  [![npm version](https://img.shields.io/npm/v/logshield-cli)](https://www.npmjs.com/package/logshield-cli)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/logshield-cli)](https://www.npmjs.com/package/logshield-cli)
5
5
  [![CI](https://github.com/afria85/LogShield/actions/workflows/ci.yml/badge.svg)](https://github.com/afria85/LogShield/actions/workflows/ci.yml)
6
+ [![license](https://img.shields.io/badge/license-Apache--2.0-3a3a3a)](https://github.com/afria85/LogShield/blob/main/LICENSE)
7
+ [![Sponsor](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-3a3a3a)](https://github.com/sponsors/afria85)
6
8
 
7
9
  Your logs already contain secrets. You just don't see them.
8
10
 
@@ -95,6 +97,29 @@ It is designed to be **predictable, conservative, and safe for production pipeli
95
97
  The website and documentation live in the `/docs` directory.
96
98
  They are deployed to **https://logshield.dev** via Vercel.
97
99
 
100
+ ## Project links
101
+
102
+ - Website: https://logshield.dev
103
+ - Docs: https://logshield.dev/docs.html
104
+ - GitHub: https://github.com/afria85/LogShield
105
+ - Sponsor: https://github.com/sponsors/afria85
106
+
107
+ ## Local preview (website)
108
+
109
+ To preview the website on your computer:
110
+
111
+ ```bash
112
+ npm run dev
113
+ ```
114
+
115
+ To preview on a phone/tablet on the same Wi-Fi:
116
+
117
+ ```bash
118
+ npm run dev:lan
119
+ ```
120
+
121
+ Then open the printed LAN URL on your device.
122
+
98
123
  ---
99
124
 
100
125
  ## Why LogShield exists
@@ -193,6 +218,8 @@ logshield scan [file]
193
218
 
194
219
  If a file is not provided and input is piped, LogShield automatically reads from **STDIN**.
195
220
 
221
+ Note: the npm package ships the CLI only; there is no supported JS API surface.
222
+
196
223
  ---
197
224
 
198
225
  ## CLI Flags
@@ -213,7 +240,7 @@ If a file is not provided and input is piped, LogShield automatically reads from
213
240
  Print a compact redaction summary
214
241
 
215
242
  - `--json`
216
- JSON output (cannot be combined with `--dry-run`)
243
+ JSON output (can be combined with `--dry-run`; output is empty in dry-run)
217
244
 
218
245
  - `--version`
219
246
  Print CLI version
@@ -371,9 +398,16 @@ Structured output for tooling and automation:
371
398
  logshield scan --json < logs.txt
372
399
  ```
373
400
 
401
+ Detection-only JSON (safe to serialize; no log content):
402
+
403
+ ```bash
404
+ logshield scan --json --dry-run < logs.txt
405
+ ```
406
+
374
407
  Notes:
375
408
 
376
- - `--json` **cannot** be combined with `--dry-run`
409
+ - `--json` can be combined with `--dry-run` for machine-readable detection
410
+ - In `--dry-run` JSON mode, `output` is intentionally an empty string
377
411
  - Usage errors exit with code `2`
378
412
  - Output is always newline-terminated
379
413
 
@@ -426,7 +460,7 @@ Depending on rules and mode:
426
460
  LogShield guarantees:
427
461
 
428
462
  - Deterministic output
429
- - Stable behavior within **v0.4.x**
463
+ - Stable behavior within the current minor line **v0.5.x**
430
464
  - No runtime dependencies
431
465
  - Snapshot-tested and contract-tested
432
466
  - No telemetry
@@ -457,3 +491,16 @@ It is a **last-line safety net**, not a primary defense.
457
491
  ## License
458
492
 
459
493
  Apache-2.0
494
+ ---
495
+
496
+ ## Contributing
497
+
498
+ See `CONTRIBUTING.md`.
499
+
500
+ ## Security
501
+
502
+ See `SECURITY.md`.
503
+
504
+ ## Support
505
+
506
+ See `SUPPORT.md`.
@@ -36,22 +36,24 @@ var readInput_exports = {};
36
36
  __export(readInput_exports, {
37
37
  readInput: () => readInput
38
38
  });
39
- async function readInput(file) {
39
+ async function readInput(file, opts) {
40
40
  if (file) {
41
41
  if (!import_node_fs.default.existsSync(file)) {
42
42
  throw new Error(`File not found: ${file}`);
43
43
  }
44
44
  return import_node_fs.default.readFileSync(file, "utf8");
45
45
  }
46
- if (!process.stdin.isTTY) {
46
+ const stdin = opts?.stdin ?? process.stdin;
47
+ const forceStdin = Boolean(opts?.forceStdin);
48
+ if (!stdin.isTTY || forceStdin) {
47
49
  return new Promise((resolve, reject) => {
48
50
  let data = "";
49
- process.stdin.setEncoding("utf8");
50
- process.stdin.on("data", (chunk) => {
51
+ stdin.setEncoding?.("utf8");
52
+ stdin.on("data", (chunk) => {
51
53
  data += chunk;
52
54
  });
53
- process.stdin.on("end", () => resolve(data));
54
- process.stdin.on("error", reject);
55
+ stdin.on("end", () => resolve(data));
56
+ stdin.on("error", reject);
55
57
  });
56
58
  }
57
59
  throw new Error("No input provided");
@@ -503,7 +505,7 @@ function sanitizeLog(input, options) {
503
505
  const matches = [];
504
506
  if (ctx.dryRun) {
505
507
  applyRules(input, allRules, ctx, matches);
506
- return { output: input, matches };
508
+ return { output: "", matches };
507
509
  }
508
510
  const output = applyRules(input, allRules, ctx, matches);
509
511
  return { output, matches };
@@ -522,8 +524,18 @@ var { writeOutput: writeOutput2 } = (init_writeOutput(), __toCommonJS(writeOutpu
522
524
  var { printSummary: printSummary2 } = (init_summary(), __toCommonJS(summary_exports));
523
525
  var { sanitizeLog: sanitizeLog2 } = (init_sanitizeLog(), __toCommonJS(sanitizeLog_exports));
524
526
  var rawArgs = process.argv.slice(2).map((arg) => arg === "-h" ? "--help" : arg);
527
+ var ALLOWED_FLAGS = /* @__PURE__ */ new Set([
528
+ "--strict",
529
+ "--dry-run",
530
+ "--stdin",
531
+ "--fail-on-detect",
532
+ "--json",
533
+ "--summary",
534
+ "--version",
535
+ "--help"
536
+ ]);
525
537
  function getVersion() {
526
- return true ? "0.4.4" : "unknown";
538
+ return true ? "0.5.0" : "unknown";
527
539
  }
528
540
  function printHelp() {
529
541
  process.stdout.write(`Usage: logshield scan [file]
@@ -550,14 +562,23 @@ function writeErr(message) {
550
562
  function parseArgs(args) {
551
563
  const flags = /* @__PURE__ */ new Set();
552
564
  const positionals = [];
565
+ const unknownFlags = [];
553
566
  for (const arg of args) {
554
- if (arg.startsWith("--")) {
555
- flags.add(arg);
567
+ if (arg.startsWith("-")) {
568
+ if (arg.startsWith("--")) {
569
+ if (!ALLOWED_FLAGS.has(arg)) {
570
+ unknownFlags.push(arg);
571
+ } else {
572
+ flags.add(arg);
573
+ }
574
+ } else {
575
+ unknownFlags.push(arg);
576
+ }
556
577
  } else {
557
578
  positionals.push(arg);
558
579
  }
559
580
  }
560
- return { flags, positionals };
581
+ return { flags, positionals, unknownFlags };
561
582
  }
562
583
  function isStdinPiped() {
563
584
  return !process.stdin.isTTY;
@@ -596,16 +617,23 @@ function exitUsageError(message) {
596
617
  process.exit(2);
597
618
  }
598
619
  async function main() {
599
- if (rawArgs.length === 0 || rawArgs.includes("--help")) {
620
+ if (rawArgs.length === 0) {
621
+ printHelp();
622
+ process.exit(0);
623
+ }
624
+ const { flags, positionals, unknownFlags } = parseArgs(rawArgs);
625
+ if (unknownFlags.length > 0) {
626
+ exitUsageError(`Unknown flag: ${unknownFlags[0]}`);
627
+ }
628
+ if (flags.has("--help")) {
600
629
  printHelp();
601
630
  process.exit(0);
602
631
  }
603
- if (rawArgs.includes("--version")) {
632
+ if (flags.has("--version")) {
604
633
  process.stdout.write(`logshield v${getVersion()}
605
634
  `);
606
635
  process.exit(0);
607
636
  }
608
- const { flags, positionals } = parseArgs(rawArgs);
609
637
  const command = positionals[0];
610
638
  if (command !== "scan") {
611
639
  exitUsageError("Unknown command");
@@ -622,16 +650,22 @@ async function main() {
622
650
  if (useStdin && file) {
623
651
  exitUsageError("Cannot read from both STDIN and file");
624
652
  }
625
- if (dryRun && json) {
626
- exitUsageError("--dry-run cannot be used with --json");
627
- }
628
653
  if (json && summary) {
629
654
  exitUsageError("--summary cannot be used with --json");
630
655
  }
631
656
  try {
632
- const input = await readInput2(useStdin ? void 0 : file);
657
+ const input = await readInput2(useStdin ? void 0 : file, {
658
+ forceStdin: stdinFlag
659
+ });
633
660
  const result = sanitizeLog2(input, { strict, dryRun });
634
661
  if (dryRun) {
662
+ if (json) {
663
+ writeOutput2(result, { json: true });
664
+ if (failOnDetect && result.matches.length > 0) {
665
+ process.exit(1);
666
+ }
667
+ process.exit(0);
668
+ }
635
669
  renderDryRunReport(result.matches);
636
670
  if (failOnDetect && result.matches.length > 0) {
637
671
  process.exit(1);
package/package.json CHANGED
@@ -1,35 +1,63 @@
1
1
  {
2
2
  "name": "logshield-cli",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
7
  "logshield": "dist/cli/index.cjs"
8
8
  },
9
9
  "files": [
10
- "dist",
10
+ "dist/cli",
11
11
  "README.md",
12
12
  "CHANGELOG.md",
13
13
  "LICENSE"
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "node scripts/build-cli.cjs",
17
- "build:web": "vite build --outDir dist-web",
18
17
  "build:blog": "node scripts/build-blog.js",
19
- "dev:web": "vite",
20
18
  "typecheck": "tsc -p tsconfig.core.json && tsc -p tsconfig.cli.json --noEmit",
21
19
  "pretest": "npm run build",
22
20
  "test": "vitest",
21
+ "lint": "npm run typecheck",
22
+ "clean:dist": "node scripts/clean-dist.mjs",
23
+ "prepack": "npm run clean:dist && npm run build",
24
+ "pack:check": "npm pack --dry-run",
25
+ "release:check": "npm run prepublish:check && npm run pack:check",
23
26
  "prepublish:check": "npm run typecheck && npm test",
24
- "prepublishOnly": "npm run prepublish:check"
27
+ "prepublishOnly": "npm run prepublish:check",
28
+ "dev": "node scripts/dev-docs-server.mjs",
29
+ "dev:lan": "node scripts/dev-docs-server.mjs --host 0.0.0.0"
25
30
  },
26
31
  "devDependencies": {
27
32
  "@types/node": "^25.0.3",
28
- "autoprefixer": "^10.4.23",
29
33
  "esbuild": "^0.25.0",
30
- "postcss": "^8.5.6",
31
- "tailwindcss": "^3.4.19",
32
34
  "typescript": "^5.9.3",
33
35
  "vitest": "^4.0.0"
36
+ },
37
+ "description": "Deterministic, rule-based CLI to sanitize secrets from logs. No AI. No cloud. No config.",
38
+ "keywords": [
39
+ "log-sanitization",
40
+ "secret-detection",
41
+ "redaction",
42
+ "security",
43
+ "devops",
44
+ "cli",
45
+ "deterministic",
46
+ "no-ai"
47
+ ],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/afria85/LogShield.git"
51
+ },
52
+ "homepage": "https://logshield.dev",
53
+ "bugs": {
54
+ "url": "https://github.com/afria85/LogShield/issues"
55
+ },
56
+ "funding": {
57
+ "type": "github",
58
+ "url": "https://github.com/sponsors/afria85"
59
+ },
60
+ "engines": {
61
+ "node": ">=18"
34
62
  }
35
63
  }
Binary file
Binary file
Binary file
@@ -1 +0,0 @@
1
- *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family: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}:root{--color-bg-primary: 248 250 252;--color-bg-secondary: 241 245 249;--color-bg-card: 255 255 255;--color-text-primary: 15 23 42;--color-text-secondary: 71 85 105;--color-text-muted: 148 163 184;--color-border: 226 232 240;--color-accent: 59 130 246;--color-accent-hover: 37 99 235;--gradient-start: 239 246 255;--gradient-mid: 238 242 255;--gradient-end: 248 250 252}*{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}@media (prefers-color-scheme: dark){*{--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}}html{scroll-behavior:smooth}body{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}@media (prefers-color-scheme: dark){body{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity, 1))}}body{margin:0;padding:0;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:JetBrains Mono,Fira Code,source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}@media (prefers-color-scheme: dark){::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}}::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity, 1))}@media (prefers-color-scheme: dark){::-webkit-scrollbar-thumb{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity, 1))}}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(148 163 184 / var(--tw-bg-opacity, 1))}@media (prefers-color-scheme: dark){::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity, 1))}}::-moz-selection{background-color:#3b82f64d;--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}::selection{background-color:#3b82f64d;--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}@media (prefers-color-scheme: dark){::-moz-selection{--tw-text-opacity: 1;color:rgb(219 234 254 / var(--tw-text-opacity, 1))}::selection{--tw-text-opacity: 1;color:rgb(219 234 254 / var(--tw-text-opacity, 1))}}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes pulseGlow{0%,to{box-shadow:0 0 #3b82f666}50%{box-shadow:0 0 20px 5px #3b82f633}}@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}to{background-position:0% 50%}}@keyframes float{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes shine{0%{background-position:-200% center}to{background-position:200% center}}@keyframes bounceUp{0%,to{transform:translateY(0)}50%{transform:translateY(-4px)}}@media print{.no-print{display:none!important}body{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}}:root{--bg: #0f1115;--panel: #151922;--border: #252a36;--text: #e6e8eb;--muted: #9aa1ad;--accent: #ff4d4f}*{box-sizing:border-box}body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;background:var(--bg);color:var(--text)}.ls-root{min-height:100vh;display:flex;flex-direction:column}.ls-header{padding:16px 24px;border-bottom:1px solid var(--border)}.ls-header h1{margin:0;font-size:20px}.ls-header p{margin:4px 0 0;color:var(--muted);font-size:13px}.ls-controls{display:flex;gap:16px;align-items:center;padding:12px 24px;border-bottom:1px solid var(--border);font-size:13px}.ls-controls label{display:flex;align-items:center;gap:6px;cursor:pointer}.ls-controls button{margin-left:auto;background:transparent;border:1px solid var(--border);color:var(--text);padding:6px 12px;cursor:pointer}.ls-controls button:hover{border-color:var(--accent)}.ls-main{flex:1;display:grid;grid-template-columns:1fr 1fr}.ls-input{width:100%;height:100%;resize:none;border:none;outline:none;padding:16px;background:var(--panel);color:var(--text);border-right:1px solid var(--border);font-size:13px;line-height:1.5}.ls-output{padding:16px;background:#0c0f14;font-size:13px;line-height:1.5;white-space:pre-wrap;overflow:auto}.ls-highlight{background:#ff4d4f40;color:#fff}.ls-footer{padding:8px 24px;border-top:1px solid var(--border);font-size:12px;color:var(--muted)}