Index: lan-frontend/package-lock.json
===================================================================
--- lan-frontend/package-lock.json	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
+++ lan-frontend/package-lock.json	(revision ba17441aef626e29462dd37a6d1d317687b77768)
@@ -20,9 +20,25 @@
         "@types/react-dom": "^19.2.3",
         "@vitejs/plugin-react": "^5.1.1",
+        "autoprefixer": "^10.4.23",
         "eslint": "^9.39.1",
         "eslint-plugin-react-hooks": "^7.0.1",
         "eslint-plugin-react-refresh": "^0.4.24",
         "globals": "^16.5.0",
+        "postcss": "^8.5.6",
+        "tailwindcss": "^3.4.17",
         "vite": "^7.2.4"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
@@ -1016,4 +1032,42 @@
       "license": "MIT"
     },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/@react-oauth/google": {
       "version": "0.13.4",
@@ -1614,4 +1668,45 @@
       }
     },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/anymatch/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/argparse": {
       "version": "2.0.1",
@@ -1621,4 +1716,41 @@
       "license": "Python-2.0"
     },
+    "node_modules/autoprefixer": {
+      "version": "10.4.23",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+      "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "browserslist": "^4.28.1",
+        "caniuse-lite": "^1.0.30001760",
+        "fraction.js": "^5.3.4",
+        "picocolors": "^1.1.1",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
@@ -1638,4 +1770,17 @@
       }
     },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/brace-expansion": {
       "version": "1.1.12",
@@ -1647,4 +1792,17 @@
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
       }
     },
@@ -1693,4 +1851,14 @@
       }
     },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/caniuse-lite": {
       "version": "1.0.30001760",
@@ -1743,4 +1911,42 @@
       }
     },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/clsx": {
       "version": "2.1.1",
@@ -1772,4 +1978,14 @@
       "license": "MIT"
     },
+    "node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/concat-map": {
       "version": "0.0.1",
@@ -1799,4 +2015,17 @@
       "engines": {
         "node": ">= 8"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
       }
     },
@@ -1957,4 +2186,18 @@
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
       "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
       "dev": true,
       "license": "MIT"
@@ -2239,4 +2482,34 @@
       "license": "MIT"
     },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/fast-json-stable-stringify": {
       "version": "2.1.0",
@@ -2253,4 +2526,14 @@
       "license": "MIT"
     },
+    "node_modules/fastq": {
+      "version": "1.20.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+      "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
     "node_modules/fdir": {
       "version": "6.5.0",
@@ -2284,4 +2567,17 @@
       }
     },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/find-up": {
       "version": "5.0.0",
@@ -2322,4 +2618,18 @@
       "license": "ISC"
     },
+    "node_modules/fraction.js": {
+      "version": "5.3.4",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+      "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/rawify"
+      }
+    },
     "node_modules/fsevents": {
       "version": "2.3.3",
@@ -2337,4 +2647,14 @@
       }
     },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/gensync": {
       "version": "1.0.0-beta.2",
@@ -2381,4 +2701,17 @@
       "engines": {
         "node": ">=8"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
@@ -2456,4 +2789,33 @@
       }
     },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-extglob": {
       "version": "2.1.1",
@@ -2479,4 +2841,14 @@
       }
     },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
     "node_modules/isexe": {
       "version": "2.0.0",
@@ -2486,4 +2858,14 @@
       "license": "ISC"
     },
+    "node_modules/jiti": {
+      "version": "1.21.7",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+      "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
@@ -2577,4 +2959,24 @@
       }
     },
+    "node_modules/lilconfig": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+      "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/locate-path": {
       "version": "6.0.0",
@@ -2610,4 +3012,41 @@
       }
     },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/micromatch/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
     "node_modules/minimatch": {
       "version": "3.1.2",
@@ -2629,4 +3068,16 @@
       "dev": true,
       "license": "MIT"
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
     },
     "node_modules/nanoid": {
@@ -2662,4 +3113,34 @@
       "dev": true,
       "license": "MIT"
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
     },
     "node_modules/optionator": {
@@ -2746,4 +3227,11 @@
       }
     },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/picocolors": {
       "version": "1.1.1",
@@ -2764,4 +3252,24 @@
       "funding": {
         "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
       }
     },
@@ -2795,4 +3303,131 @@
       }
     },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+      "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-load-config": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+      "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "lilconfig": "^3.0.0",
+        "yaml": "^2.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      },
+      "peerDependencies": {
+        "postcss": ">=8.0.9",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "postcss": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "postcss-selector-parser": "^6.1.1"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/prelude-ls": {
       "version": "1.2.1",
@@ -2814,4 +3449,25 @@
         "node": ">=6"
       }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
     },
     "node_modules/react": {
@@ -2874,4 +3530,40 @@
       "engines": {
         "node": ">=0.10.0"
+      }
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/readdirp/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
@@ -2927,4 +3619,25 @@
       "license": "MIT"
     },
+    "node_modules/resolve": {
+      "version": "1.22.11",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+      "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.16.1",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/resolve-from": {
       "version": "4.0.0",
@@ -2935,4 +3648,15 @@
       "engines": {
         "node": ">=4"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
       }
     },
@@ -2979,4 +3703,28 @@
       }
     },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
     "node_modules/scheduler": {
       "version": "0.27.0",
@@ -3041,4 +3789,27 @@
       }
     },
+    "node_modules/sucrase": {
+      "version": "3.35.1",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+      "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "tinyglobby": "^0.2.11",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
     "node_modules/supports-color": {
       "version": "7.2.0",
@@ -3054,4 +3825,78 @@
       }
     },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.4.17",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+      "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.6.0",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.2",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.21.6",
+        "lilconfig": "^3.1.3",
+        "micromatch": "^4.0.8",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.1.1",
+        "postcss": "^8.4.47",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.2",
+        "postcss-nested": "^6.2.0",
+        "postcss-selector-parser": "^6.1.2",
+        "resolve": "^1.22.8",
+        "sucrase": "^3.35.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/tiny-invariant": {
       "version": "1.3.3",
@@ -3076,4 +3921,24 @@
         "url": "https://github.com/sponsors/SuperchupuDev"
       }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+      "dev": true,
+      "license": "Apache-2.0"
     },
     "node_modules/type-check": {
@@ -3139,4 +4004,11 @@
         "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
       }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/victory-vendor": {
@@ -3270,4 +4142,20 @@
       "license": "ISC"
     },
+    "node_modules/yaml": {
+      "version": "2.8.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
+      "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/eemeli"
+      }
+    },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
Index: lan-frontend/package.json
===================================================================
--- lan-frontend/package.json	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
+++ lan-frontend/package.json	(revision ba17441aef626e29462dd37a6d1d317687b77768)
@@ -22,8 +22,11 @@
     "@types/react-dom": "^19.2.3",
     "@vitejs/plugin-react": "^5.1.1",
+    "autoprefixer": "^10.4.23",
     "eslint": "^9.39.1",
     "eslint-plugin-react-hooks": "^7.0.1",
     "eslint-plugin-react-refresh": "^0.4.24",
     "globals": "^16.5.0",
+    "postcss": "^8.5.6",
+    "tailwindcss": "^3.4.17",
     "vite": "^7.2.4"
   }
Index: lan-frontend/postcss.config.js
===================================================================
--- lan-frontend/postcss.config.js	(revision ba17441aef626e29462dd37a6d1d317687b77768)
+++ lan-frontend/postcss.config.js	(revision ba17441aef626e29462dd37a6d1d317687b77768)
@@ -0,0 +1,6 @@
+export default {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+}
Index: lan-frontend/src/App.jsx
===================================================================
--- lan-frontend/src/App.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
+++ lan-frontend/src/App.jsx	(revision ba17441aef626e29462dd37a6d1d317687b77768)
@@ -1,4 +1,5 @@
-import React, { useEffect, useMemo, useState } from "react";
+import React, {useEffect, useMemo, useState} from "react";
 import "./styles/theme.css";
+import "./styles/netinel.css";
 
 import Login from "./components/Login";
@@ -7,270 +8,329 @@
 import ComputerDetailsModal from "./components/ComputerDetailsModal";
 import AIAssistantDrawer from "./components/AIAssistantDrawer";
+import NetIntelViz from "./components/NetIntelViz.jsx";
+import SideMenu from "./components/SideMenu";
+
 
 export default function App() {
-  const [stats, setStats] = useState(null);
-  const [computers, setComputers] = useState([]);
-  const [loading, setLoading] = useState(true);
-  const [lastUpdated, setLastUpdated] = useState(null);
-
-  const [query, setQuery] = useState("");
-  const [status, setStatus] = useState("");
-
-  const [selectedComputer, setSelectedComputer] = useState(null);
-  const [aiOpen, setAiOpen] = useState(false);
-  const [aiScope, setAiScope] = useState(null);
-  const [selectedEnv, setSelectedEnv] = useState("default");
-
-  const [me, setMe] = useState(null);
-  const [meLoading, setMeLoading] = useState(true);
-
-  // NEW: environment setting (switch)
-  const [saveProcHistory, setSaveProcHistory] = useState(false);
-
-  async function loadMe() {
-    setMeLoading(true);
-    try {
-      const r = await fetch("/api/me", { credentials: "include" });
-      if (r.ok) {
-        const data = await r.json();
-        setMe(data.user);
-      } else {
-        setMe(null);
-      }
-    } catch (e) {
-      console.error("loadMe error", e);
-      setMe(null);
-    } finally {
-      setMeLoading(false);
+    const [stats, setStats] = useState(null);
+    const [computers, setComputers] = useState([]);
+    const [loading, setLoading] = useState(true);
+    const [lastUpdated, setLastUpdated] = useState(null);
+
+    const [query, setQuery] = useState("");
+    const [status, setStatus] = useState("");
+
+    const [selectedComputer, setSelectedComputer] = useState(null);
+    const [aiOpen, setAiOpen] = useState(false);
+    const [aiScope, setAiScope] = useState(null);
+    const [selectedEnv, setSelectedEnv] = useState("default");
+
+    const [me, setMe] = useState(null);
+    const [meLoading, setMeLoading] = useState(true);
+
+    const [activeView, setActiveView] = useState("dashboard");
+// "dashboard" | "visualizations" | "account"
+
+
+    // NEW: environment setting (switch)
+    const [saveProcHistory, setSaveProcHistory] = useState(false);
+
+    async function loadMe() {
+        setMeLoading(true);
+        try {
+            const r = await fetch("/api/me", {credentials: "include"});
+            if (r.ok) {
+                const data = await r.json();
+                setMe(data.user);
+            } else {
+                setMe(null);
+            }
+        } catch (e) {
+            console.error("loadMe error", e);
+            setMe(null);
+        } finally {
+            setMeLoading(false);
+        }
     }
-  }
-
-  async function refresh() {
-    try {
-      setLoading(true);
-      const [s, c] = await Promise.all([
-        fetch("/api/stats", { credentials: "include" }).then((r) => r.json()),
-        fetch("/api/computers", {
-          credentials: "include",
-          headers: { "X-Env": selectedEnv },
-        }).then((r) => r.json()),
-      ]);
-
-      setStats(s);
-      setComputers(Array.isArray(c) ? c : []);
-      setLastUpdated(new Date().toISOString());
-    } catch (e) {
-      console.error("refresh error", e);
-    } finally {
-      setLoading(false);
+
+    async function refresh() {
+        try {
+            setLoading(true);
+            const [s, c] = await Promise.all([
+                fetch("/api/stats", {credentials: "include"}).then((r) => r.json()),
+                fetch("/api/computers", {
+                    credentials: "include",
+                    headers: {"X-Env": selectedEnv},
+                }).then((r) => r.json()),
+            ]);
+
+            setStats(s);
+            setComputers(Array.isArray(c) ? c : []);
+            setLastUpdated(new Date().toISOString());
+        } catch (e) {
+            console.error("refresh error", e);
+        } finally {
+            setLoading(false);
+        }
     }
-  }
-
-  // NEW: load env settings (switch state) for selected env
-  async function loadEnvSettings(env) {
-    try {
-      // само admin ќе има пристап (ако не си admin, ќе падне во else и ќе стане false)
-      const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, {
-        credentials: "include",
-      });
-
-      if (r.ok) {
-        const data = await r.json();
-        setSaveProcHistory(!!data.save_process_history);
-      } else {
-        setSaveProcHistory(false);
-      }
-    } catch (e) {
-      console.error("loadEnvSettings error", e);
-      setSaveProcHistory(false);
+
+    // NEW: load env settings (switch state) for selected env
+    async function loadEnvSettings(env) {
+        try {
+            // само admin ќе има пристап (ако не си admin, ќе падне во else и ќе стане false)
+            const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, {
+                credentials: "include",
+            });
+
+            if (r.ok) {
+                const data = await r.json();
+                setSaveProcHistory(!!data.save_process_history);
+            } else {
+                setSaveProcHistory(false);
+            }
+        } catch (e) {
+            console.error("loadEnvSettings error", e);
+            setSaveProcHistory(false);
+        }
     }
-  }
-
-  // NEW: set env setting (toggle)
-  async function setEnvProcessHistory(env, enabled) {
-    // optimistic UI
-    setSaveProcHistory(enabled);
-
-    try {
-      const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, {
-        method: "POST",
-        credentials: "include",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify({ save_process_history: enabled }),
-      });
-
-      if (r.ok) {
-        const data = await r.json();
-        setSaveProcHistory(!!data.save_process_history);
-      } else {
-        // rollback
-        setSaveProcHistory(!enabled);
-        console.error("setEnvProcessHistory failed", await r.text());
-      }
-    } catch (e) {
-      setSaveProcHistory(!enabled);
-      console.error("setEnvProcessHistory error", e);
+
+    // NEW: set env setting (toggle)
+    async function setEnvProcessHistory(env, enabled) {
+        // optimistic UI
+        setSaveProcHistory(enabled);
+
+        try {
+            const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, {
+                method: "POST",
+                credentials: "include",
+                headers: {"Content-Type": "application/json"},
+                body: JSON.stringify({save_process_history: enabled}),
+            });
+
+            if (r.ok) {
+                const data = await r.json();
+                setSaveProcHistory(!!data.save_process_history);
+            } else {
+                // rollback
+                setSaveProcHistory(!enabled);
+                console.error("setEnvProcessHistory failed", await r.text());
+            }
+        } catch (e) {
+            setSaveProcHistory(!enabled);
+            console.error("setEnvProcessHistory error", e);
+        }
     }
-  }
-
-  // on mount: check session
-  useEffect(() => {
-    loadMe();
-  }, []);
-
-  // when logged in OR env changed, refresh data + load env switch state
-  useEffect(() => {
-    if (!me) return;
-    loadEnvSettings(selectedEnv);
-    refresh();
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [me, selectedEnv]);
-
-  const filtered = useMemo(() => {
-    const q = query.trim().toLowerCase();
-    return (computers || [])
-      .filter((c) => (status ? c.status === status : true))
-      .filter((c) => {
-        if (!q) return true;
-        const hay = [c.name, c.user, c.ip, c.os, c.status]
-          .filter(Boolean)
-          .join(" ")
-          .toLowerCase();
-        return hay.includes(q);
-      });
-  }, [computers, query, status]);
-
-  // Gate UI
-  if (meLoading) return <div style={{ padding: 20 }}>Loading…</div>;
-  if (!me) return <Login onDone={loadMe} />;
-
-  return (
-    <div className="app-wrap">
-      <TopBar
-        onOpenAI={() => setAiOpen(true)}
-        onRefresh={refresh}
-        lastUpdated={lastUpdated}
-        onEnvChange={(env) => setSelectedEnv(env)}
-        selectedEnv={selectedEnv}
-        me={me}
-        onLoggedOut={loadMe}
-        // NEW props for switch:
-        saveProcHistory={saveProcHistory}
-        onToggleProcHistory={(enabled) => setEnvProcessHistory(selectedEnv, enabled)}
-      />
-
-      {/* STATS */}
-      <div className="stats-grid">
-        <div className="stat-card">
-          <div className="label">Total Computers</div>
-          <div className="value">{computers.length}</div>
-          <div className="sub">Monitored Systems</div>
+
+    // on mount: check session
+    useEffect(() => {
+        loadMe();
+    }, []);
+
+    // when logged in OR env changed, refresh data + load env switch state
+    useEffect(() => {
+        if (!me) return;
+        loadEnvSettings(selectedEnv);
+        refresh();
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [me, selectedEnv]);
+
+    const filtered = useMemo(() => {
+        const q = query.trim().toLowerCase();
+        return (computers || [])
+            .filter((c) => (status ? c.status === status : true))
+            .filter((c) => {
+                if (!q) return true;
+                const hay = [c.name, c.user, c.ip, c.os, c.status]
+                    .filter(Boolean)
+                    .join(" ")
+                    .toLowerCase();
+                return hay.includes(q);
+            });
+    }, [computers, query, status]);
+
+    // Gate UI
+    if (meLoading) return <div style={{padding: 20}}>Loading…</div>;
+    if (!me) return <Login onDone={loadMe}/>;
+
+    return (
+        <div className="app-shell">
+            <div className="layout">
+                <SideMenu activeView={activeView} setActiveView={setActiveView}/>
+                <main className="content">
+                    {/* TopBar + pages */}
+                </main>
+            </div>
+
+            <div className="cont">
+                <TopBar
+                    onOpenAI={() => setAiOpen(true)}
+                    onRefresh={refresh}
+                    lastUpdated={lastUpdated}
+                    onEnvChange={(env) => setSelectedEnv(env)}
+                    selectedEnv={selectedEnv}
+                    me={me}
+                    onLoggedOut={loadMe}
+                    saveProcHistory={saveProcHistory}
+                    onToggleProcHistory={(enabled) => setEnvProcessHistory(selectedEnv, enabled)}
+                />
+
+                <div className="app-main">
+                    {/* ====== VIEW SWITCH ====== */}
+                    {activeView === "dashboard" && (
+                        <>
+                            {/* STATS */}
+                            <div className="stats-grid">
+                                <div className="stat-card">
+                                    <div className="label">Total Computers</div>
+                                    <div className="value">{computers.length}</div>
+                                    <div className="sub">Monitored Systems</div>
+                                </div>
+
+                                <div className="stat-card">
+                                    <div className="label">Sysmon Events (24h)</div>
+                                    <div className="value">{stats?.total_sysmon_events ?? "—"}</div>
+                                    <div className="sub">Security Events</div>
+                                </div>
+
+                                <div className="stat-card">
+                                    <div className="label">Network Connections (24h)</div>
+                                    <div className="value">{stats?.total_network_connections ?? "—"}</div>
+                                    <div className="sub">Captured Connections</div>
+                                </div>
+                            </div>
+
+                            {/* FILTERS */}
+                            <div className="panel" style={{marginBottom: 14}}>
+                                <div className="panel-head">
+                                    <div className="panel-title">Filters</div>
+                                    <div style={{color: "var(--dim)", fontSize: 12}}>
+                                        {loading ? "Refreshing…" : `${filtered.length} shown`}
+                                    </div>
+                                </div>
+                                <div className="panel-body">
+                                    <div className="filters">
+                                        <input
+                                            className="input"
+                                            value={query}
+                                            onChange={(e) => setQuery(e.target.value)}
+                                            placeholder="Search by name / user / ip / os…"
+                                        />
+
+                                        <select
+                                            className="select"
+                                            value={status}
+                                            onChange={(e) => setStatus(e.target.value)}
+                                        >
+                                            <option value="">All status</option>
+                                            <option value="online">Online</option>
+                                            <option value="idle">Idle</option>
+                                            <option value="offline">Offline</option>
+                                        </select>
+
+                                        <button className="btn primary" onClick={refresh}>
+                                            Refresh
+                                        </button>
+                                    </div>
+                                </div>
+                            </div>
+
+                            {/* COMPUTERS */}
+                            <div className="panel">
+                                <div className="panel-head">
+                                    <div className="panel-title">Connected Computers</div>
+                                    <div style={{display: "flex", gap: 10, alignItems: "center"}}>
+                                        <span className="pill">{filtered.length} computers</span>
+                                        <button
+                                            className="btn"
+                                            onClick={() => {
+                                                setQuery("");
+                                                setStatus("");
+                                            }}
+                                        >
+                                            Reset
+                                        </button>
+                                    </div>
+                                </div>
+                                <div className="panel-body">
+                                    <div className="computers-grid">
+                                        {filtered.map((c) => (
+                                            <ComputerCard
+                                                key={c.name}
+                                                c={c}
+                                                onOpen={(pc) => {
+                                                    setSelectedComputer(pc);
+                                                    setAiScope(pc.name);
+                                                }}
+                                            />
+                                        ))}
+                                        {filtered.length === 0 && (
+                                            <div style={{color: "var(--dim)"}}>
+                                                No computers match filter.
+                                            </div>
+                                        )}
+                                    </div>
+                                </div>
+                            </div>
+                        </>
+                    )}
+
+                    {activeView === "visualizations" && (
+                        <>
+                            <div className="panel" style={{marginBottom: 14}}>
+                                <div className="panel-head">
+                                    <div className="panel-title">Visualizations</div>
+                                    <div style={{color: "var(--dim)", fontSize: 12}}>
+                                        Network graphs / charts / tables
+                                    </div>
+                                </div>
+                                <div className="panel-body">
+                                    <NetIntelViz baseUrl="http://localhost:5555"/>
+                                </div>
+                            </div>
+                        </>
+                    )}
+
+                    {activeView === "account" && (
+                        <div className="panel">
+                            <div className="panel-head">
+                                <div className="panel-title">Account</div>
+                                <div style={{color: "var(--dim)", fontSize: 12}}>
+                                    Profile info
+                                </div>
+                            </div>
+                            <div className="panel-body">
+                                <div style={{display: "grid", gap: 10}}>
+                                    <div className="pill">Email: {me.email}</div>
+                                    <div className="pill">Role: {me.role}</div>
+                                    <div className="pill">Tenant ID: {me.tenant_id}</div>
+                                    <div className="pill">Name: {me.name || "—"}</div>
+                                </div>
+                            </div>
+                        </div>
+                    )}
+                </div>
+
+                {/* DETAILS MODAL */}
+                <ComputerDetailsModal
+                    open={!!selectedComputer}
+                    computer={selectedComputer}
+                    onClose={() => setSelectedComputer(null)}
+                    onAskAI={(name) => {
+                        setAiScope(name);
+                        setAiOpen(true);
+                    }}
+                />
+
+                {/* AI DRAWER */}
+                <AIAssistantDrawer
+                    open={aiOpen}
+                    onClose={() => setAiOpen(false)}
+                    computers={computers}
+                    scope={aiScope}
+                    setScope={setAiScope}
+                    apiBase={import.meta.env.VITE_API_BASE}
+                />
+            </div>
         </div>
-
-        <div className="stat-card">
-          <div className="label">Sysmon Events (24h)</div>
-          <div className="value">{stats?.total_sysmon_events ?? "—"}</div>
-          <div className="sub">Security Events</div>
-        </div>
-
-        <div className="stat-card">
-          <div className="label">Network Connections (24h)</div>
-          <div className="value">{stats?.total_network_connections ?? "—"}</div>
-          <div className="sub">Captured Connections</div>
-        </div>
-      </div>
-
-      {/* FILTERS */}
-      <div className="panel" style={{ marginBottom: 14 }}>
-        <div className="panel-head">
-          <div className="panel-title">Filters</div>
-          <div style={{ color: "var(--dim)", fontSize: 12 }}>
-            {loading ? "Refreshing…" : `${filtered.length} shown`}
-          </div>
-        </div>
-        <div className="panel-body">
-          <div className="filters">
-            <input
-              className="input"
-              value={query}
-              onChange={(e) => setQuery(e.target.value)}
-              placeholder="Search by name / user / ip / os…"
-            />
-
-            <select
-              className="select"
-              value={status}
-              onChange={(e) => setStatus(e.target.value)}
-            >
-              <option value="">All status</option>
-              <option value="online">Online</option>
-              <option value="idle">Idle</option>
-              <option value="offline">Offline</option>
-            </select>
-
-            <button className="btn primary" onClick={refresh}>
-              Refresh
-            </button>
-          </div>
-        </div>
-      </div>
-
-      {/* COMPUTERS */}
-      <div className="panel">
-        <div className="panel-head">
-          <div className="panel-title">Connected Computers</div>
-          <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
-            <span className="pill">{filtered.length} computers</span>
-            <button
-              className="btn"
-              onClick={() => {
-                setQuery("");
-                setStatus("");
-              }}
-            >
-              Reset
-            </button>
-          </div>
-        </div>
-        <div className="panel-body">
-          <div className="computers-grid">
-            {filtered.map((c) => (
-              <ComputerCard
-                key={c.name}
-                c={c}
-                onOpen={(pc) => {
-                  setSelectedComputer(pc);
-                  setAiScope(pc.name);
-                }}
-              />
-            ))}
-            {filtered.length === 0 && (
-              <div style={{ color: "var(--dim)" }}>No computers match filter.</div>
-            )}
-          </div>
-        </div>
-      </div>
-
-      {/* DETAILS MODAL */}
-      <ComputerDetailsModal
-        open={!!selectedComputer}
-        computer={selectedComputer}
-        onClose={() => setSelectedComputer(null)}
-        onAskAI={(name) => {
-          setAiScope(name);
-          setAiOpen(true);
-        }}
-      />
-
-      {/* AI DRAWER */}
-      <AIAssistantDrawer
-        open={aiOpen}
-        onClose={() => setAiOpen(false)}
-        computers={computers}
-        scope={aiScope}
-        setScope={setAiScope}
-        apiBase={import.meta.env.VITE_API_BASE}
-      />
-    </div>
-  );
+    );
 }
Index: lan-frontend/src/components/NetIntelViz.jsx
===================================================================
--- lan-frontend/src/components/NetIntelViz.jsx	(revision ba17441aef626e29462dd37a6d1d317687b77768)
+++ lan-frontend/src/components/NetIntelViz.jsx	(revision ba17441aef626e29462dd37a6d1d317687b77768)
@@ -0,0 +1,653 @@
+import React, { useEffect, useMemo, useState } from "react";
+import {
+  LineChart,
+  Line,
+  XAxis,
+  YAxis,
+  Tooltip,
+  Legend,
+  ResponsiveContainer,
+  CartesianGrid,
+} from "recharts";
+
+const VIZ = {
+  STATS: "stats",
+  COMPUTERS: "computers",
+  COMPUTER_DETAILS: "computer_details",
+  NET_GRAPH: "net_graph",
+};
+
+function fmtTs(ts) {
+  if (!ts) return "";
+  return String(ts).replace("T", " ").replace("Z", "");
+}
+
+async function fetchJson(url, { method = "GET", headers = {}, body } = {}) {
+  const res = await fetch(url, {
+    method,
+    credentials: "include",
+    headers: {
+      "Content-Type": "application/json",
+      ...headers,
+    },
+    body: body ? JSON.stringify(body) : undefined,
+  });
+
+  const text = await res.text();
+  let data;
+  try {
+    data = text ? JSON.parse(text) : null;
+  } catch {
+    data = { raw: text };
+  }
+  if (!res.ok) {
+    const msg = data?.error || data?.message || res.statusText;
+    throw new Error(msg);
+  }
+  return data;
+}
+
+/** ✅ Use existing theme.css look: panel / btn / select / input / pill */
+function Panel({ title, right, children, subtitle }) {
+  return (
+    <div className="panel" style={{ marginBottom: 14 }}>
+      <div className="panel-head">
+        <div>
+          <div className="panel-title">{title}</div>
+          {subtitle ? (
+            <div style={{ color: "var(--dim)", fontSize: 12, marginTop: 4 }}>{subtitle}</div>
+          ) : null}
+        </div>
+        {right}
+      </div>
+      <div className="panel-body">{children}</div>
+    </div>
+  );
+}
+
+function Select({ label, value, onChange, options, disabled }) {
+  return (
+    <label style={{ display: "grid", gap: 6 }}>
+      <div style={{ fontSize: 12, color: "var(--dim)" }}>{label}</div>
+      <select
+        className="select"
+        value={value}
+        disabled={disabled}
+        onChange={(e) => onChange(e.target.value)}
+      >
+        {options.map((o) => (
+          <option key={o.value} value={o.value}>
+            {o.label}
+          </option>
+        ))}
+      </select>
+    </label>
+  );
+}
+
+function Pill({ children }) {
+  return <span className="pill">{children}</span>;
+}
+
+function Table({ columns, rows, emptyText = "Нема податоци." }) {
+  return (
+    <div style={{ overflow: "auto" }}>
+      <table className="table">
+        <thead>
+          <tr>
+            {columns.map((c) => (
+              <th key={c.key}>{c.title}</th>
+            ))}
+          </tr>
+        </thead>
+        <tbody>
+          {rows.length === 0 ? (
+            <tr>
+              <td colSpan={columns.length} style={{ color: "var(--dim)", padding: 12 }}>
+                {emptyText}
+              </td>
+            </tr>
+          ) : (
+            rows.map((r, idx) => (
+              <tr key={idx}>
+                {columns.map((c) => (
+                  <td key={c.key}>{c.render ? c.render(r) : String(r[c.key] ?? "")}</td>
+                ))}
+              </tr>
+            ))
+          )}
+        </tbody>
+      </table>
+    </div>
+  );
+}
+
+export default function NetIntelViz({ baseUrl = "" }) {
+  const [loading, setLoading] = useState(false);
+  const [err, setErr] = useState("");
+
+  const [me, setMe] = useState(null);
+  const [envs, setEnvs] = useState(["default"]);
+  const [env, setEnv] = useState("default");
+
+  const [viz, setViz] = useState(VIZ.NET_GRAPH);
+
+  const [stats, setStats] = useState(null);
+  const [computers, setComputers] = useState([]);
+  const [computerName, setComputerName] = useState("");
+  const [computerDetails, setComputerDetails] = useState(null);
+
+  const [sinceHours, setSinceHours] = useState("24");
+  const [graph, setGraph] = useState(null);
+
+  const api = useMemo(() => (baseUrl || "").replace(/\/+$/, ""), [baseUrl]);
+
+  const envOptions = useMemo(() => {
+    const uniq = [...new Set(envs)];
+    return uniq.map((e) => ({ value: e, label: e }));
+  }, [envs]);
+
+  const computerOptions = useMemo(() => {
+    const opts = computers.map((c) => ({ value: c.name, label: c.name }));
+    return [{ value: "", label: "all" }, ...opts];
+  }, [computers]);
+
+  const vizOptions = [
+    { value: VIZ.STATS, label: "Stats (24h)" },
+    { value: VIZ.COMPUTERS, label: "Computers" },
+    { value: VIZ.COMPUTER_DETAILS, label: "Computer details" },
+    { value: VIZ.NET_GRAPH, label: "Network graph" },
+  ];
+
+  async function loadMe() {
+    const data = await fetchJson(`${api}/api/me`);
+    setMe(data?.user || null);
+  }
+
+  async function loadEnvs() {
+    try {
+      const data = await fetchJson(`${api}/api/admin/environments`);
+      const list = data?.environments?.length ? data.environments : ["default"];
+      setEnvs(list);
+      if (!list.includes(env)) setEnv(list[0] || "default");
+    } catch {
+      setEnvs(["default"]);
+      setEnv("default");
+    }
+  }
+
+  async function loadStats(selectedEnv) {
+    const data = await fetchJson(`${api}/api/stats`, {
+      headers: { "X-Env": selectedEnv || env || "default" },
+    });
+    setStats(data);
+  }
+
+  async function loadComputers(selectedEnv) {
+    const data = await fetchJson(`${api}/api/computers`, {
+      headers: { "X-Env": selectedEnv || env || "default" },
+    });
+    setComputers(Array.isArray(data) ? data : []);
+  }
+
+  async function loadComputerDetails(name) {
+    if (!name) {
+      setComputerDetails(null);
+      return;
+    }
+    const data = await fetchJson(`${api}/api/computer/${encodeURIComponent(name)}`, {
+      headers: { "X-Env": env || "default" },
+    });
+    setComputerDetails(data);
+  }
+
+  async function loadNetGraph() {
+    const qs = new URLSearchParams();
+    qs.set("env", env || "default");
+    qs.set("since_hours", String(parseInt(sinceHours || "24", 10) || 24));
+    if (computerName) qs.set("computer", computerName);
+
+    const data = await fetchJson(`${api}/api/graph/net?${qs.toString()}`, {
+      headers: { "X-Env": env || "default" },
+    });
+    setGraph(data);
+  }
+  function MiniBipartiteGraph({ nodes = [], edges = [] }) {
+  const comps = nodes.filter((n) => (n.subTitle || "").toLowerCase().includes("computer"));
+  const ips = nodes.filter((n) => (n.subTitle || "").toLowerCase().includes("remote"));
+
+  const width = 1100;
+  const height = 520;
+  const pad = 40;
+
+  const compPos = comps.map((n, i) => ({
+    ...n,
+    x: pad,
+    y: pad + (i * (height - 2 * pad)) / Math.max(1, comps.length - 1),
+  }));
+
+  const ipPos = ips.map((n, i) => ({
+    ...n,
+    x: width - pad,
+    y: pad + (i * (height - 2 * pad)) / Math.max(1, ips.length - 1),
+  }));
+
+  const pos = new Map();
+  [...compPos, ...ipPos].forEach((n) => pos.set(n.id, n));
+
+  const shownEdges = edges.slice(0, 120);
+
+  return (
+    <div style={{ overflow: "auto" }}>
+      <svg width={width} height={height} style={{ borderRadius: 16, background: "rgba(255,255,255,0.03)" }}>
+        {shownEdges.map((e, idx) => {
+          const s = pos.get(e.source);
+          const t = pos.get(e.target);
+          if (!s || !t) return null;
+          const v = Number(e.mainStat || 1);
+          const strokeWidth = Math.max(1, Math.min(6, Math.log2(v + 1)));
+          return (
+            <line
+              key={idx}
+              x1={s.x}
+              y1={s.y}
+              x2={t.x}
+              y2={t.y}
+              stroke="rgba(120,180,255,0.25)"
+              strokeWidth={strokeWidth}
+            />
+          );
+        })}
+
+        {compPos.map((n) => (
+          <g key={n.id}>
+            <circle cx={n.x} cy={n.y} r={6} fill="rgba(255,255,255,0.9)" />
+            <text x={n.x + 12} y={n.y + 4} fontSize="11" fill="rgba(255,255,255,0.85)">
+              {n.title}
+            </text>
+          </g>
+        ))}
+
+        {ipPos.map((n) => (
+          <g key={n.id}>
+            <circle cx={n.x} cy={n.y} r={6} fill="rgba(80,160,255,0.95)" />
+            <text x={n.x - 12} y={n.y + 4} textAnchor="end" fontSize="11" fill="rgba(255,255,255,0.85)">
+              {n.title}
+            </text>
+          </g>
+        ))}
+      </svg>
+
+      <div style={{ color: "var(--dim)", fontSize: 12, marginTop: 10 }}>
+        Tip: ако има премногу линии, намали Hours или избери конкретен Computer.
+      </div>
+    </div>
+  );
+}
+
+
+  async function refreshAll(nextViz = viz, nextEnv = env) {
+    setErr("");
+    setLoading(true);
+    try {
+      if (nextViz === VIZ.STATS) {
+        await loadStats(nextEnv);
+      } else if (nextViz === VIZ.COMPUTERS) {
+        await loadComputers(nextEnv);
+      } else if (nextViz === VIZ.COMPUTER_DETAILS) {
+        await loadComputers(nextEnv);
+        if (computerName) await loadComputerDetails(computerName);
+      } else if (nextViz === VIZ.NET_GRAPH) {
+        await loadComputers(nextEnv);
+        await loadNetGraph();
+      }
+    } catch (e) {
+      setErr(e.message || "Грешка при вчитување.");
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  useEffect(() => {
+    (async () => {
+      setLoading(true);
+      setErr("");
+      try {
+        await loadMe();
+        await loadEnvs();
+        await refreshAll(VIZ.NET_GRAPH, env);
+      } catch (e) {
+        setErr(e.message || "Неуспешно поврзување.");
+      } finally {
+        setLoading(false);
+      }
+    })();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [api]);
+
+  const historySeries = useMemo(() => {
+    const hist = computerDetails?.history || [];
+    const rows = [...hist].reverse().slice(-80);
+    return rows.map((r) => ({
+      t: fmtTs(r.timestamp),
+      cpu: Number(r.cpu_usage ?? 0),
+      ram: Number(r.ram_usage ?? 0),
+      disk: Number(r.disk_usage ?? 0),
+      sent: Number(r.network_sent_mb ?? 0),
+      recv: Number(r.network_recv_mb ?? 0),
+    }));
+  }, [computerDetails]);
+
+  const nodes = graph?.nodes || [];
+  const edges = graph?.edges || [];
+  const meta = graph?.meta || {};
+
+  return (
+    <div>
+      {/* HEADER / CONTROLS */}
+      <Panel
+        title="Visualizations"
+        subtitle={me ? `Logged: ${me.email} • tenant: ${me.tenant_id} • role: ${me.role}` : "Not logged"}
+        right={
+          <div style={{ display: "flex", gap: 10, alignItems: "end", flexWrap: "wrap" }}>
+            <Select
+              label="Env"
+              value={env}
+              disabled={loading}
+              onChange={(v) => {
+                setEnv(v);
+                refreshAll(viz, v);
+              }}
+              options={envOptions}
+            />
+
+            <Select
+              label="View"
+              value={viz}
+              disabled={loading}
+              onChange={(v) => {
+                setViz(v);
+                refreshAll(v, env);
+              }}
+              options={vizOptions}
+            />
+
+            {(viz === VIZ.COMPUTER_DETAILS || viz === VIZ.NET_GRAPH) && (
+              <Select
+                label="Computer"
+                value={computerName}
+                disabled={loading}
+                onChange={(v) => {
+                  setComputerName(v);
+                  if (viz === VIZ.COMPUTER_DETAILS) loadComputerDetails(v).catch((e) => setErr(e.message));
+                  if (viz === VIZ.NET_GRAPH) loadNetGraph().catch((e) => setErr(e.message));
+                }}
+                options={computerOptions}
+              />
+            )}
+
+            {viz === VIZ.NET_GRAPH && (
+              <Select
+                label="Hours"
+                value={sinceHours}
+                disabled={loading}
+                onChange={(v) => setSinceHours(v)}
+                options={[
+                  { value: "1", label: "1" },
+                  { value: "6", label: "6" },
+                  { value: "12", label: "12" },
+                  { value: "24", label: "24" },
+                  { value: "48", label: "48" },
+                  { value: "72", label: "72" },
+                ]}
+              />
+            )}
+
+            <button className="btn primary" disabled={loading} onClick={() => refreshAll(viz, env)}>
+              {loading ? "Loading…" : "Refresh"}
+            </button>
+          </div>
+        }
+      >
+        {err ? (
+          <div style={{ color: "#ffb4b4", background: "rgba(255,0,0,0.08)", padding: 10, borderRadius: 12 }}>
+            {err}
+          </div>
+        ) : (
+          <div style={{ color: "var(--dim)", fontSize: 12 }}>
+            Избери “View” за да смениш што се прикажува.
+          </div>
+        )}
+      </Panel>
+      <Panel title="Graph (SVG)" subtitle="Computers left, IPs right, lines = connections">
+  <MiniBipartiteGraph nodes={nodes} edges={edges} />
+</Panel>
+
+
+      {/* STATS */}
+      {viz === VIZ.STATS && (
+        <Panel title="Stats (24h)" subtitle="From /api/stats">
+          <div className="stats-grid" style={{ marginTop: 0 }}>
+            <div className="stat-card">
+              <div className="label">Sysmon Events</div>
+              <div className="value">{stats?.total_sysmon_events ?? "—"}</div>
+              <div className="sub">Last 24 hours</div>
+            </div>
+            <div className="stat-card">
+              <div className="label">Network Connections</div>
+              <div className="value">{stats?.total_network_connections ?? "—"}</div>
+              <div className="sub">Last 24 hours</div>
+            </div>
+            <div className="stat-card">
+              <div className="label">Server Time</div>
+              <div className="value" style={{ fontSize: 18 }}>
+                {fmtTs(stats?.server_time) || "—"}
+              </div>
+              <div className="sub">Now</div>
+            </div>
+          </div>
+        </Panel>
+      )}
+
+      {/* COMPUTERS */}
+      {viz === VIZ.COMPUTERS && (
+        <Panel title="Computers" subtitle="From /api/computers">
+          <Table
+            columns={[
+              { key: "name", title: "Name" },
+              { key: "user", title: "User" },
+              { key: "ip", title: "IP" },
+              { key: "os", title: "OS" },
+              { key: "status", title: "Status" },
+              { key: "avg_cpu", title: "Avg CPU" },
+              { key: "avg_ram", title: "Avg RAM" },
+              { key: "recent_logs", title: "Logs(24h)" },
+              { key: "recent_sysmon", title: "Sysmon(24h)" },
+              { key: "last_seen", title: "Last seen", render: (r) => fmtTs(r.last_seen) },
+            ]}
+            rows={computers}
+            emptyText="No computers for this env."
+          />
+        </Panel>
+      )}
+
+      {/* COMPUTER DETAILS */}
+      {viz === VIZ.COMPUTER_DETAILS && (
+        <>
+          <Panel
+            title="Computer details"
+            subtitle="From /api/computer/<name>"
+            right={
+              <button
+                className="btn"
+                disabled={loading || !computerName}
+                onClick={() => loadComputerDetails(computerName).catch((e) => setErr(e.message))}
+              >
+                Reload
+              </button>
+            }
+          >
+            {!computerName ? (
+              <div style={{ color: "var(--dim)" }}>Select a computer.</div>
+            ) : !computerDetails ? (
+              <div style={{ color: "var(--dim)" }}>Loading…</div>
+            ) : (
+              <div className="stats-grid" style={{ marginTop: 0 }}>
+                <div className="stat-card">
+                  <div className="label">Name</div>
+                  <div className="value">{computerDetails.computer?.name}</div>
+                  <div className="sub">{computerDetails.computer?.ip} • {computerDetails.computer?.os}</div>
+                </div>
+                <div className="stat-card">
+                  <div className="label">User</div>
+                  <div className="value">{computerDetails.computer?.user || "—"}</div>
+                  <div className="sub">
+                    {fmtTs(computerDetails.computer?.first_seen)} → {fmtTs(computerDetails.computer?.last_seen)}
+                  </div>
+                </div>
+                <div className="stat-card">
+                  <div className="label">Total logs</div>
+                  <div className="value">{computerDetails.computer?.total_logs ?? "—"}</div>
+                  <div className="sub">Env: {computerDetails.computer?.env_name || env}</div>
+                </div>
+              </div>
+            )}
+          </Panel>
+
+          <Panel title="Performance charts" subtitle="Last ~80 history rows">
+            {historySeries.length === 0 ? (
+              <div style={{ color: "var(--dim)" }}>No history data.</div>
+            ) : (
+              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
+                <div style={{ height: 280 }}>
+                  <div style={{ marginBottom: 8, color: "var(--dim)", fontSize: 12 }}>CPU / RAM / Disk</div>
+                  <ResponsiveContainer width="100%" height="100%">
+                    <LineChart data={historySeries}>
+                      <CartesianGrid strokeDasharray="3 3" />
+                      <XAxis dataKey="t" hide />
+                      <YAxis />
+                      <Tooltip />
+                      <Legend />
+                      <Line type="monotone" dataKey="cpu" dot={false} />
+                      <Line type="monotone" dataKey="ram" dot={false} />
+                      <Line type="monotone" dataKey="disk" dot={false} />
+                    </LineChart>
+                  </ResponsiveContainer>
+                </div>
+
+                <div style={{ height: 280 }}>
+                  <div style={{ marginBottom: 8, color: "var(--dim)", fontSize: 12 }}>Network MB sent/recv</div>
+                  <ResponsiveContainer width="100%" height="100%">
+                    <LineChart data={historySeries}>
+                      <CartesianGrid strokeDasharray="3 3" />
+                      <XAxis dataKey="t" hide />
+                      <YAxis />
+                      <Tooltip />
+                      <Legend />
+                      <Line type="monotone" dataKey="sent" dot={false} />
+                      <Line type="monotone" dataKey="recv" dot={false} />
+                    </LineChart>
+                  </ResponsiveContainer>
+                </div>
+              </div>
+            )}
+          </Panel>
+
+          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
+            <Panel title="Processes (current)" subtitle="computer_processes_current (limit 200)">
+              <Table
+                columns={[
+                  { key: "pid", title: "PID" },
+                  { key: "name", title: "Name" },
+                  { key: "username", title: "User" },
+                  { key: "cpu_percent", title: "CPU%" },
+                  { key: "memory_mb", title: "MemMB" },
+                  { key: "timestamp", title: "Time", render: (r) => fmtTs(r.timestamp) },
+                ]}
+                rows={computerDetails?.recent_processes || []}
+              />
+            </Panel>
+
+            <Panel title="Sysmon events" subtitle="sysmon_events (limit 200)">
+              <Table
+                columns={[
+                  { key: "event_id", title: "Event" },
+                  { key: "event_type", title: "Type" },
+                  {
+                    key: "message",
+                    title: "Message",
+                    render: (r) =>
+                      (r.message || "").slice(0, 70) + ((r.message || "").length > 70 ? "…" : ""),
+                  },
+                  { key: "timestamp", title: "Time", render: (r) => fmtTs(r.timestamp) },
+                ]}
+                rows={computerDetails?.sysmon_events || []}
+              />
+            </Panel>
+          </div>
+
+          <Panel title="Network connections" subtitle="network_connections (limit 100)">
+            <Table
+              columns={[
+                { key: "pid", title: "PID" },
+                { key: "process_name", title: "Process" },
+                { key: "local_address", title: "Local" },
+                { key: "remote_address", title: "Remote" },
+                { key: "status", title: "Status" },
+                { key: "timestamp", title: "Time", render: (r) => fmtTs(r.timestamp) },
+              ]}
+              rows={computerDetails?.network_connections || []}
+            />
+          </Panel>
+        </>
+      )}
+
+      {/* NET GRAPH */}
+      {viz === VIZ.NET_GRAPH && (
+        <>
+          <Panel
+            title="Network graph (computer → remote IP)"
+            subtitle="From /api/graph/net"
+            right={
+              <button className="btn" disabled={loading} onClick={() => loadNetGraph().catch((e) => setErr(e.message))}>
+                Reload graph
+              </button>
+            }
+          >
+            <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
+              <Pill>env: {meta?.env || env}</Pill>
+              <Pill>hours: {meta?.since_hours ?? sinceHours}</Pill>
+              <Pill>computer: {meta?.computer || "all"}</Pill>
+              <Pill>nodes: {nodes.length}</Pill>
+              <Pill>edges: {edges.length}</Pill>
+            </div>
+          </Panel>
+
+          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
+            <Panel title="Top edges" subtitle="Each row = number of connections">
+              <Table
+                columns={[
+                  { key: "source", title: "Source" },
+                  { key: "target", title: "Target" },
+                  { key: "mainStat", title: "Count" },
+                ]}
+                rows={edges.slice(0, 80)}
+                emptyText="No edges (maybe no traffic in that period)."
+              />
+            </Panel>
+
+            <Panel title="Nodes" subtitle="Computers + remote IP">
+              <Table
+                columns={[
+                  { key: "title", title: "Title" },
+                  { key: "subTitle", title: "Type" },
+                ]}
+                rows={nodes.slice(0, 120)}
+              />
+            </Panel>
+          </div>
+        </>
+      )}
+    </div>
+  );
+}
Index: lan-frontend/src/components/SideMenu.jsx
===================================================================
--- lan-frontend/src/components/SideMenu.jsx	(revision ba17441aef626e29462dd37a6d1d317687b77768)
+++ lan-frontend/src/components/SideMenu.jsx	(revision ba17441aef626e29462dd37a6d1d317687b77768)
@@ -0,0 +1,101 @@
+import React, {useEffect, useRef, useState} from "react";
+
+function Item({active, icon, label, onClick, collapsed}) {
+    return (
+        <button
+            onClick={onClick}
+            className={`side-item ${active ? "active" : ""}`}
+            title={label}
+            type="button"
+        >
+            <span className="side-icon">{icon}</span>
+            {!collapsed && <span className="side-label">{label}</span>}
+        </button>
+    );
+}
+
+export default function SideMenu({activeView, setActiveView}) {
+    const [open, setOpen] = useState(false);
+    const rootRef = useRef(null);
+
+    // close on click outside (ако е open преку click)
+    useEffect(() => {
+        function onDocClick(e) {
+            if (!open) return;
+            if (!rootRef.current) return;
+            if (!rootRef.current.contains(e.target)) setOpen(false);
+        }
+
+        document.addEventListener("mousedown", onDocClick);
+        return () => document.removeEventListener("mousedown", onDocClick);
+    }, [open]);
+
+    const collapsed = !open;
+
+    return (
+        <aside
+            ref={rootRef}
+            className={`side ${open ? "open" : "collapsed"}`}
+            onMouseEnter={() => setOpen(true)}   // hover open
+            onMouseLeave={() => setOpen(false)}  // hover close
+        >
+            {/* мал “handle” што се гледа секогаш */}
+            <button
+                className="side-handle"
+                type="button"
+                onClick={() => setOpen((v) => !v)}  // click toggle (ако не сакаш click, избриши ова)
+                aria-label="Toggle menu"
+                title="Menu"
+            >
+                {open ? "◀" : "▶"}
+            </button>
+
+            <div className="side-inner">
+                <div className="side-brand">
+                    <div className="side-brand-dot"/>
+                    {!collapsed && (
+                        <div className="side-brand-text">
+                            <div className="side-brand-title">netIntel</div>
+                            <div className="side-brand-sub">tenant dashboard</div>
+                        </div>
+                    )}
+                </div>
+
+
+                <div className="side-section">
+                    {/*<div className="side-section-title">MAIN</div>*/}
+
+                    <Item
+                        collapsed={collapsed}
+                        active={activeView === "dashboard"}
+                        icon="🖥️"
+                        label="Dashboard"
+                        onClick={() => setActiveView("dashboard")}
+                    />
+
+                    <Item
+                        collapsed={collapsed}
+                        active={activeView === "visualizations"}
+                        icon="📈"
+                        label="Visualizations"
+                        onClick={() => setActiveView("visualizations")}
+                    />
+
+                    <Item
+                        collapsed={collapsed}
+                        active={activeView === "account"}
+                        icon="👤"
+                        label="Account"
+                        onClick={() => setActiveView("account")}
+                    />
+                </div>
+
+                {!collapsed && (
+                    <div className="side-footer">
+                        <div className="side-footer-hint">Tip: користи Visualizations за графови</div>
+                    </div>
+                )}
+            </div>
+        </aside>
+    );
+}
Index: lan-frontend/src/styles/netinel.css
===================================================================
--- lan-frontend/src/styles/netinel.css	(revision ba17441aef626e29462dd37a6d1d317687b77768)
+++ lan-frontend/src/styles/netinel.css	(revision ba17441aef626e29462dd37a6d1d317687b77768)
@@ -0,0 +1,125 @@
+/* netintel.css - basic styling without Tailwind */
+
+.netintel-wrap {
+  max-width: 1100px;
+  margin: 0 auto;
+  padding: 16px;
+  color: #e5e7eb;
+  font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
+}
+
+.netintel-title {
+  font-size: 24px;
+  font-weight: 700;
+  margin-bottom: 6px;
+}
+
+.netintel-sub {
+  font-size: 13px;
+  opacity: 0.85;
+  margin-bottom: 14px;
+}
+
+.netintel-toolbar {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  align-items: end;
+  margin-bottom: 14px;
+}
+
+.netintel-field {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.netintel-label {
+  font-size: 11px;
+  opacity: 0.85;
+}
+
+.netintel-select,
+.netintel-btn {
+  background: rgba(255,255,255,0.06);
+  border: 1px solid rgba(255,255,255,0.15);
+  color: #e5e7eb;
+  border-radius: 10px;
+  padding: 8px 10px;
+  font-size: 14px;
+  outline: none;
+}
+
+.netintel-btn {
+  cursor: pointer;
+}
+
+.netintel-btn:hover {
+  background: rgba(255,255,255,0.10);
+}
+
+.netintel-card {
+  background: rgba(255,255,255,0.06);
+  border: 1px solid rgba(255,255,255,0.12);
+  border-radius: 16px;
+  padding: 14px;
+  margin-bottom: 14px;
+}
+
+.netintel-card h3 {
+  margin: 0 0 6px;
+  font-size: 16px;
+}
+
+.netintel-card p {
+  margin: 0 0 10px;
+  font-size: 13px;
+  opacity: 0.85;
+}
+
+.netintel-pill {
+  display: inline-flex;
+  align-items: center;
+  padding: 2px 8px;
+  border-radius: 999px;
+  font-size: 12px;
+  background: rgba(255,255,255,0.08);
+  border: 1px solid rgba(255,255,255,0.12);
+  margin-right: 6px;
+  margin-bottom: 6px;
+}
+
+.netintel-tablewrap {
+  overflow: auto;
+  border-radius: 12px;
+  border: 1px solid rgba(255,255,255,0.12);
+}
+
+.netintel-table {
+  width: 100%;
+  border-collapse: collapse;
+  min-width: 700px;
+}
+
+.netintel-table th {
+  text-align: left;
+  font-weight: 600;
+  font-size: 13px;
+  background: rgba(0,0,0,0.25);
+  padding: 10px;
+}
+
+.netintel-table td {
+  padding: 10px;
+  border-top: 1px solid rgba(255,255,255,0.08);
+  font-size: 13px;
+}
+
+.netintel-error {
+  background: rgba(239, 68, 68, 0.12);
+  border: 1px solid rgba(239, 68, 68, 0.25);
+  color: #fecaca;
+  padding: 10px 12px;
+  border-radius: 12px;
+  margin-bottom: 12px;
+}
Index: lan-frontend/src/styles/theme.css
===================================================================
--- lan-frontend/src/styles/theme.css	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
+++ lan-frontend/src/styles/theme.css	(revision ba17441aef626e29462dd37a6d1d317687b77768)
@@ -3,4 +3,7 @@
    Dark Navy + Light Blue
    ========================= */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
 
 :root{
@@ -548,2 +551,264 @@
   border-color: rgba(88, 167, 255, .35);
 }
+
+/* ====== LAYOUT with LEFT SIDEBAR ====== */
+.app-shell {
+  display: grid;
+  grid-template-columns: 240px 1fr;
+  min-height: 100vh;
+}
+
+.app-main {
+  padding: 14px;
+}
+
+/* Sidebar */
+.side {
+  background: rgba(255,255,255,0.04);
+  border-right: 1px solid rgba(255,255,255,0.10);
+  padding: 14px;
+  position: sticky;
+  top: 0;
+  height: 100vh;
+}
+
+.side-brand {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+  padding: 10px;
+  border: 1px solid rgba(255,255,255,0.10);
+  border-radius: 14px;
+  background: rgba(0,0,0,0.12);
+  margin-bottom: 14px;
+}
+
+.side-brand-dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 999px;
+  background: #7dd3fc;
+  box-shadow: 0 0 0 4px rgba(125,211,252,0.15);
+}
+
+.side-brand-title {
+  font-weight: 700;
+  letter-spacing: 0.2px;
+}
+
+.side-brand-sub {
+  font-size: 12px;
+  color: var(--dim);
+}
+
+.side-section-title {
+  font-size: 11px;
+  color: var(--dim);
+  margin: 12px 8px 8px;
+  letter-spacing: 0.12em;
+}
+
+.side-item {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 12px;
+  border-radius: 12px;
+  border: 1px solid transparent;
+  background: transparent;
+  color: inherit;
+  cursor: pointer;
+  text-align: left;
+}
+
+.side-item:hover {
+  background: rgba(255,255,255,0.06);
+  border-color: rgba(255,255,255,0.08);
+}
+
+.side-item.active {
+  background: rgba(125,211,252,0.12);
+  border-color: rgba(125,211,252,0.25);
+}
+
+.side-icon {
+  width: 26px;
+  display: grid;
+  place-items: center;
+  font-size: 18px;
+}
+
+.side-label {
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.side-footer {
+  position: absolute;
+  bottom: 12px;
+  left: 14px;
+  right: 14px;
+  padding: 10px;
+  border-radius: 12px;
+  border: 1px solid rgba(255,255,255,0.10);
+  background: rgba(0,0,0,0.12);
+}
+
+.side-footer-hint {
+  font-size: 12px;
+  color: var(--dim);
+}
+/* ==== Collapsible Side Menu ==== */
+.side {
+  position: sticky;
+  top: 0;
+  height: 100vh;
+  z-index: 20;
+
+  width: 260px;
+  transition: width 180ms ease;
+  border-right: 1px solid rgba(255,255,255,0.08);
+  background: rgba(6, 12, 24, 0.72);
+  backdrop-filter: blur(10px);
+}
+
+.side.collapsed {
+  width: 54px; /* само тенка лента */
+}
+
+.side-inner {
+  height: 100%;
+  padding: 14px 10px;
+  display: flex;
+  flex-direction: column;
+}
+
+.side-handle {
+  position: absolute;
+  left: 6px;
+  top: 10px;
+  width: 42px;
+  height: 34px;
+  border-radius: 12px;
+  border: 1px solid rgba(255,255,255,0.12);
+  background: rgba(255,255,255,0.06);
+  color: rgba(255,255,255,0.9);
+  cursor: pointer;
+}
+
+.side-brand {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px;
+  margin-top: 38px; /* за да не се судри со handle */
+  border-radius: 16px;
+  background: rgba(255,255,255,0.03);
+  border: 1px solid rgba(255,255,255,0.06);
+}
+
+.side-brand-dot {
+  width: 12px;
+  height: 12px;
+  border-radius: 999px;
+  background: rgba(80, 160, 255, 0.95);
+  box-shadow: 0 0 18px rgba(80,160,255,0.45);
+}
+
+.side-brand-text { min-width: 0; }
+.side-brand-title { font-weight: 800; color: rgba(255,255,255,0.92); }
+.side-brand-sub { font-size: 12px; color: rgba(255,255,255,0.55); }
+
+.side-section { margin-top: 14px; }
+.side-section-title {
+  font-size: 11px;
+  letter-spacing: 0.12em;
+  color: rgba(255,255,255,0.45);
+  padding: 10px;
+}
+
+.side-item {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 12px;
+  margin: 6px 0;
+  border-radius: 14px;
+  border: 1px solid transparent;
+  background: transparent;
+  color: rgba(255,255,255,0.82);
+  cursor: pointer;
+  text-align: left;
+}
+
+.side-item:hover {
+  background: rgba(255,255,255,0.06);
+  border-color: rgba(255,255,255,0.08);
+}
+
+.side-item.active {
+  background: rgba(80,160,255,0.16);
+  border-color: rgba(80,160,255,0.25);
+}
+
+.side-icon {
+  width: 28px;
+  display: grid;
+  place-items: center;
+  font-size: 18px;
+}
+
+.side-label {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.side-footer {
+  margin-top: auto;
+  padding: 10px;
+  color: rgba(255,255,255,0.55);
+  font-size: 12px;
+}
+.layout { display: flex; }
+.content { flex: 1; min-width: 0; }
+/* collapsed = центрирање иконите + тргање празни простори */
+.side.collapsed .side-inner {
+  padding: 10px 6px;
+  align-items: center;
+}
+
+.side.collapsed .side-brand {
+  width: 42px;
+  padding: 10px 0;
+  justify-content: center;
+  margin-top: 48px;
+}
+
+.side.collapsed .side-item {
+  justify-content: center;
+  padding: 10px 0;
+}
+
+.side.collapsed .side-icon {
+  width: 42px;
+}
+
+/* handle во средина (поубаво) */
+.side-handle {
+  left: 6px;
+  top: 10px;
+  width: 42px;
+  height: 34px;
+}
+
+/* кога е open да си е нормално */
+.side.open .side-inner {
+  align-items: stretch;
+}
+
+.cont{
+  margin-right: 50px;
+}
